diff --git a/.githooks/commit-msg b/.githooks/commit-msg index af7010a11f..192fc9dd12 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -44,7 +44,7 @@ test "" = "$(grep '^Signed-off-by: ' "$1" | if [ $? -ne 0 ] then printError "Please fix your commit message to match AppFlowy coding standards" - printError "https://docs.appflowy.io/docs/documentation/software-contributions/conventions/git-conventions" + printError "https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/code-submission-guidelines#commit-message-guidelines" exit 1 fi diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f71d51f263..57ebf4205f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,7 +21,7 @@ Before you mark this PR ready for review, run through this checklist! #### PR Checklist -- [ ] My code adheres to [AppFlowy's Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions) +- [ ] My code adheres to the [AppFlowy Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides) - [ ] I've listed at least one issue that this PR fixes in the description above. - [ ] I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes. - [ ] All existing tests are passing. diff --git a/.github/actions/flutter_build/action.yml b/.github/actions/flutter_build/action.yml deleted file mode 100644 index bfcb501327..0000000000 --- a/.github/actions/flutter_build/action.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Flutter Integration Test -description: Run integration tests for AppFlowy - -inputs: - os: - description: "The operating system to run the tests on" - required: true - flutter_version: - description: "The version of Flutter to use" - required: true - rust_toolchain: - description: "The version of Rust to use" - required: true - cargo_make_version: - description: "The version of cargo-make to use" - required: true - rust_target: - description: "The target to build for" - required: true - flutter_profile: - description: "The profile to build with" - required: true - -runs: - using: "composite" - - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - id: rust_toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ inputs.rust_toolchain }} - target: ${{ inputs.rust_target }} - override: true - profile: minimal - - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ inputs.flutter_version }} - cache: true - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ inputs.os }} - workspaces: | - frontend/rust-lib - cache-all-crates: true - - - uses: taiki-e/install-action@v2 - with: - tool: cargo-make@${{ inputs.cargo_make_version }}, duckscript_cli - - - 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 - cargo make appflowy-flutter-deps-tools - - - name: Build AppFlowy - working-directory: frontend - run: cargo make --profile ${{ inputs.flutter_profile }} appflowy-core-dev - shell: bash - - - name: Run code generation - working-directory: frontend - run: cargo make code_generation - shell: bash - - - name: Flutter Analyzer - working-directory: frontend/appflowy_flutter - run: flutter analyze . - shell: bash - - - name: Compress appflowy_flutter - run: tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter - shell: bash - - - uses: actions/upload-artifact@v4 - with: - name: ${{ github.run_id }}-${{ matrix.os }} - path: appflowy_flutter.tar.gz diff --git a/.github/actions/flutter_integration_test/action.yml b/.github/actions/flutter_integration_test/action.yml deleted file mode 100644 index e0fa508ade..0000000000 --- a/.github/actions/flutter_integration_test/action.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Flutter Integration Test -description: Run integration tests for AppFlowy - -inputs: - test_path: - description: "The path to the integration test file" - required: true - flutter_version: - description: "The version of Flutter to use" - required: true - rust_toolchain: - description: "The version of Rust to use" - required: true - cargo_make_version: - description: "The version of cargo-make to use" - required: true - rust_target: - description: "The target to build for" - required: true - -runs: - using: "composite" - - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - id: rust_toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ inputs.RUST_TOOLCHAIN }} - target: ${{ inputs.rust_target }} - override: true - profile: minimal - - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ inputs.flutter_version }} - cache: true - - - uses: taiki-e/install-action@v2 - with: - tool: cargo-make@${{ inputs.cargo_make_version }} - - - name: Install prerequisites - working-directory: frontend - run: | - 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 - shell: bash - - - name: Enable Flutter Desktop - run: | - flutter config --enable-linux-desktop - shell: bash - - - uses: actions/download-artifact@v4 - with: - name: ${{ github.run_id }}-ubuntu-latest - - - name: Uncompressed appflowy_flutter - run: tar -xf appflowy_flutter.tar.gz - shell: bash - - - name: Run Flutter integration tests - working-directory: frontend/appflowy_flutter - run: | - export DISPLAY=:99 - 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 diff --git a/.github/workflows/android_ci.yaml.bak b/.github/workflows/android_ci.yaml.bak deleted file mode 100644 index 81e132cbf8..0000000000 --- a/.github/workflows/android_ci.yaml.bak +++ /dev/null @@ -1,196 +0,0 @@ -name: Android CI - -on: - push: - branches: - - "main" - paths: - - ".github/workflows/mobile_ci.yaml" - - "frontend/**" - - 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 - -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 }} - - 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 - - - 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: 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 - - - 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 - - # 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: Checkout source code - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 11 - - - name: Install Rust toolchain - id: rust_toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - override: true - profile: minimal - - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - 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 diff --git a/.github/workflows/build_command.yml b/.github/workflows/build_command.yml deleted file mode 100644 index 1648953bae..0000000000 --- a/.github/workflows/build_command.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: build - -on: - repository_dispatch: - types: [build-command] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: notify appflowy_builder - run: | - platform=${{ github.event.client_payload.slash_command.args.unnamed.arg1 }} - build_name=${{ github.event.client_payload.slash_command.args.named.build_name }} - branch=${{ github.event.client_payload.slash_command.args.named.ref }} - build_type="" - arch="" - - if [ "$platform" = "android" ]; then - build_type="apk" - elif [ "$platform" = "macos" ]; then - arch="universal" - fi - - params=$(jq -n \ - --arg ref "main" \ - --arg repo "LucasXu0/AppFlowy" \ - --arg branch "$branch" \ - --arg build_name "$build_name" \ - --arg build_type "$build_type" \ - --arg arch "$arch" \ - '{ref: $ref, inputs: {repo: $repo, branch: $branch, build_name: $build_name, build_type: $build_type, arch: $arch}} | del(.inputs | .. | select(. == ""))') - - echo "params: $params" - - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/AppFlowy-IO/AppFlowy-Builder/actions/workflows/$platform.yaml/dispatches \ - -d "$params" diff --git a/.github/workflows/commit_lint.yml b/.github/workflows/commit_lint.yml index eb55922af2..4c9a5a5473 100644 --- a/.github/workflows/commit_lint.yml +++ b/.github/workflows/commit_lint.yml @@ -5,7 +5,8 @@ jobs: commitlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v4 + diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml index 51e8a2ac28..07b4e32c99 100644 --- a/.github/workflows/docker_ci.yml +++ b/.github/workflows/docker_ci.yml @@ -2,46 +2,46 @@ name: Docker-CI on: push: - branches: [ "main", "release/*" ] - pull_request: - branches: [ "main", "release/*" ] - workflow_dispatch: + branches: + - main + - release/* + paths: + - frontend/** -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + pull_request: + branches: + - main + - release/* + paths: + - frontend/** + types: + - opened + - synchronize + - reopened + - unlocked + - ready_for_review jobs: build-app: if: github.event.pull_request.draft != true + concurrency: + group: docker_ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true runs-on: ubuntu-latest steps: - 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- + uses: actions/checkout@v3 - 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..37b62a11aa 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -4,78 +4,40 @@ on: push: branches: - "main" + - "develop" - "release/*" paths: - ".github/workflows/flutter_ci.yaml" - - ".github/actions/flutter_build/**" - - "frontend/rust-lib/**" - - "frontend/appflowy_flutter/**" - - "frontend/resources/**" + - "frontend/**" + - "!frontend/appflowy_tauri/**" pull_request: branches: - "main" + - "develop" - "release/*" paths: - ".github/workflows/flutter_ci.yaml" - - ".github/actions/flutter_build/**" - - "frontend/rust-lib/**" - - "frontend/appflowy_flutter/**" - - "frontend/resources/**" + - "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 - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + FLUTTER_VERSION: "3.10.1" + RUST_TOOLCHAIN: "1.70" jobs: - prepare-linux: - if: github.event.pull_request.draft != true + build: strategy: - fail-fast: true + fail-fast: false matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, macos-latest, windows-latest] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 target: x86_64-unknown-linux-gnu - runs-on: ${{ matrix.os }} - - steps: - # the following step is required to avoid running out of space - - name: Maximize build space - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Flutter build - uses: ./.github/actions/flutter_build - with: - os: ${{ matrix.os }} - flutter_version: ${{ env.FLUTTER_VERSION }} - rust_toolchain: ${{ env.RUST_TOOLCHAIN }} - cargo_make_version: ${{ env.CARGO_MAKE_VERSION }} - rust_target: ${{ matrix.target }} - flutter_profile: ${{ matrix.flutter_profile }} - - prepare-windows: - if: github.event.pull_request.draft != true - strategy: - fail-fast: true - matrix: - os: [windows-latest] - include: + - os: macos-latest + flutter_profile: development-mac-x86_64 + target: x86_64-apple-darwin - os: windows-latest flutter_profile: development-windows-x86 target: x86_64-pc-windows-msvc @@ -83,61 +45,7 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v4 - - - name: Flutter build - uses: ./.github/actions/flutter_build - with: - os: ${{ matrix.os }} - flutter_version: ${{ env.FLUTTER_VERSION }} - DISABLE_CI_TEST_LOG: "true" - rust_toolchain: ${{ env.RUST_TOOLCHAIN }} - cargo_make_version: ${{ env.CARGO_MAKE_VERSION }} - rust_target: ${{ matrix.target }} - flutter_profile: ${{ matrix.flutter_profile }} - - prepare-macos: - if: github.event.pull_request.draft != true - strategy: - fail-fast: true - matrix: - os: [macos-latest] - include: - - os: macos-latest - flutter_profile: development-mac-x86_64 - target: x86_64-apple-darwin - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Flutter build - uses: ./.github/actions/flutter_build - with: - os: ${{ matrix.os }} - flutter_version: ${{ env.FLUTTER_VERSION }} - rust_toolchain: ${{ env.RUST_TOOLCHAIN }} - cargo_make_version: ${{ env.CARGO_MAKE_VERSION }} - rust_target: ${{ matrix.target }} - flutter_profile: ${{ matrix.flutter_profile }} - - unit_test: - needs: [prepare-linux] - if: github.event.pull_request.draft != true - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - include: - - os: ubuntu-latest - flutter_profile: development-linux-x86_64 - target: x86_64-unknown-linux-gnu - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v2 - name: Install Rust toolchain id: rust_toolchain @@ -161,21 +69,27 @@ jobs: prefix-key: ${{ matrix.os }} workspaces: | frontend/rust-lib - cache-all-crates: true - - uses: taiki-e/install-action@v2 + - uses: davidB/rust-cargo-make@v1 with: - tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}, duckscript_cli + version: '0.36.6' - name: Install prerequisites working-directory: frontend run: | + cargo install --force duckscript_cli 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 + 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 + 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: Enable Flutter Desktop @@ -188,178 +102,34 @@ jobs: git config --system core.longpaths true flutter config --enable-windows-desktop fi + dart pub global activate protoc_plugin 20.0.1 shell: bash - - uses: actions/download-artifact@v4 - with: - name: ${{ github.run_id }}-${{ matrix.os }} - - - name: Uncompress appflowy_flutter - run: tar -xf appflowy_flutter.tar.gz - - - name: Run flutter pub get - working-directory: frontend - run: cargo make pub_get - - - name: Run Flutter unit tests - env: - DISABLE_EVENT_LOG: true - DISABLE_CI_TEST_LOG: "true" + - name: Build AppFlowy working-directory: frontend run: | - if [ "$RUNNER_OS" == "macOS" ]; then - cargo make dart_unit_test - elif [ "$RUNNER_OS" == "Linux" ]; then - cargo make dart_unit_test_no_build - elif [ "$RUNNER_OS" == "Windows" ]; then - cargo make dart_unit_test_no_build - fi - shell: bash + cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev - cloud_integration_test: - needs: [prepare-linux] - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - include: - - os: ubuntu-latest - flutter_profile: development-linux-x86_64 - target: x86_64-unknown-linux-gnu - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout appflowy cloud code - uses: actions/checkout@v4 - with: - repository: AppFlowy-IO/AppFlowy-Cloud - path: AppFlowy-Cloud - - - 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 - - - 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 - - # 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: Checkout source code - uses: actions/checkout@v4 - - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - uses: taiki-e/install-action@v2 - with: - tool: cargo-make@${{ env.CARGO_MAKE_VERSION }} - - - name: Install prerequisites - working-directory: frontend - run: | - 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 - shell: bash - - - name: Enable Flutter Desktop - run: | - flutter config --enable-linux-desktop - shell: bash - - - uses: actions/download-artifact@v4 - with: - name: ${{ github.run_id }}-${{ matrix.os }} - - - name: Uncompressed appflowy_flutter - run: | - tar -xf appflowy_flutter.tar.gz - ls -al - - - name: Run flutter pub get - working-directory: frontend - run: cargo make pub_get - - - name: Run Flutter integration tests + - name: Flutter Analyzer working-directory: frontend/appflowy_flutter run: | - export DISPLAY=:99 - 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 - shell: bash + flutter analyze . - integration_test: - 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] - include: - - os: ubuntu-latest - target: "x86_64-unknown-linux-gnu" - runs-on: ${{ matrix.os }} - steps: - - name: Checkout source code - uses: actions/checkout@v4 + - name: Run Flutter unit tests + working-directory: frontend + run: | + cargo make dart_unit_test - - name: Flutter Integration Test ${{ matrix.test_number }} - uses: ./.github/actions/flutter_integration_test + - name: Upload coverage to Codecov + uses: Wandalen/wretry.action@v1.0.36 with: - test_path: integration_test/desktop_runner_${{ matrix.test_number }}.dart - flutter_version: ${{ env.FLUTTER_VERSION }} - rust_toolchain: ${{ env.RUST_TOOLCHAIN }} - cargo_make_version: ${{ env.CARGO_MAKE_VERSION }} - rust_target: ${{ matrix.target }} + action: codecov/codecov-action@v3 + with: | + name: appflowy + flags: appflowy_flutter_unit_test + fail_ci_if_error: true + verbose: true + os: ${{ matrix.os }} + attempt_limit: 5 + attempt_delay: 10000 + diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml new file mode 100644 index 0000000000..7014a3cbfd --- /dev/null +++ b/.github/workflows/integration_test.yml @@ -0,0 +1,136 @@ +name: integration test + +on: + push: + branches: + - "main" + - "release/*" + paths: + - ".github/workflows/flutter_ci.yaml" + - "frontend/**" + - "!frontend/appflowy_tauri/**" + + pull_request: + branches: + - "main" + - "release/*" + paths: + - ".github/workflows/flutter_ci.yaml" + - "frontend/**" + - "!frontend/appflowy_tauri/**" + +env: + CARGO_TERM_COLOR: always + FLUTTER_VERSION: "3.10.1" + RUST_TOOLCHAIN: "1.70" + +jobs: + tests: + strategy: + matrix: + os: [ubuntu-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: "stable-2022-04-07" + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + target: ${{ matrix.target }} + override: true + profile: minimal + + - name: Install flutter + id: flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.os }} + workspaces: | + frontend/rust-lib + + - uses: davidB/rust-cargo-make@v1 + with: + version: '0.36.6' + + - name: Install prerequisites + working-directory: frontend + run: | + cargo install --force duckscript_cli + 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 + 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: Config Flutter + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + flutter config --enable-linux-desktop + elif [ "$RUNNER_OS" == "macOS" ]; then + flutter config --enable-macos-desktop + elif [ "$RUNNER_OS" == "Windows" ]; then + flutter config --enable-windows-desktop + fi + dart pub global activate protoc_plugin 20.0.1 + shell: bash + + - name: Build Test lib + working-directory: frontend + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + cargo make --profile development-linux-x86_64 appflowy-dev + elif [ "$RUNNER_OS" == "macOS" ]; then + cargo make --profile development-mac-x86_64 appflowy-dev + elif [ "$RUNNER_OS" == "Windows" ]; then + cargo make --profile development-windows-x86 appflowy-dev + fi + shell: bash + + - name: Run AppFlowy tests + working-directory: frontend/appflowy_flutter + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + export DISPLAY=:99 + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + sudo apt-get install network-manager + flutter test integration_test/runner.dart -d Linux --coverage --verbose + elif [ "$RUNNER_OS" == "macOS" ]; then + flutter test integration_test/runner.dart -d macOS --coverage --verbose + elif [ "$RUNNER_OS" == "Windows" ]; then + flutter test integration_test/runner.dart -d Windows --coverage --verbose + fi + shell: bash + + - name: Upload coverage to Codecov + uses: Wandalen/wretry.action@v1.0.36 + with: + action: codecov/codecov-action@v3 + with: | + name: appflowy + flags: appflowy_flutter_integrateion_test + fail_ci_if_error: true + verbose: true + os: ${{ matrix.os }} + attempt_limit: 5 + attempt_delay: 10000 \ No newline at end of file diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml deleted file mode 100644 index e13863f4a7..0000000000 --- a/.github/workflows/ios_ci.yaml +++ /dev/null @@ -1,119 +0,0 @@ -name: iOS CI - -on: - push: - branches: - - "main" - paths: - - ".github/workflows/mobile_ci.yaml" - - "frontend/**" - - "!frontend/appflowy_web_app/**" - - pull_request: - branches: - - "main" - paths: - - ".github/workflows/mobile_ci.yaml" - - "frontend/**" - - "!frontend/appflowy_web_app/**" - -env: - FLUTTER_VERSION: "3.27.4" - RUST_TOOLCHAIN: "1.81.0" - -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 - - 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 - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - target: aarch64-apple-ios-sim - override: true - profile: minimal - - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: macos-latest - workspaces: | - frontend/rust-lib - - - uses: davidB/rust-cargo-make@v1 - with: - version: "0.37.15" - - - name: Install prerequisites - working-directory: frontend - run: | - rustup target install aarch64-apple-ios-sim - cargo install --force --locked duckscript_cli - cargo install cargo-lipo - cargo make appflowy-flutter-deps-tools - shell: bash - - - 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 - - - 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 }} 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/ninja_i18n.yml b/.github/workflows/ninja_i18n.yml deleted file mode 100644 index 8473f8f069..0000000000 --- a/.github/workflows/ninja_i18n.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Ninja i18n action - -on: - pull_request_target: - -# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings -permissions: - pull-requests: write - -jobs: - ninja-i18n: - name: Ninja i18n - GitHub Lint Action - runs-on: ubuntu-latest - - steps: - - name: Checkout - id: checkout - uses: actions/checkout@v4 - - - name: Run Ninja i18n - id: ninja-i18n - uses: opral/ninja-i18n-action@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4582ffa74..d20122c7c8 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.10.1" + RUST_TOOLCHAIN: "1.70" jobs: create-release: @@ -18,7 +18,7 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v2 - name: Build release notes run: | @@ -35,15 +35,13 @@ jobs: release_name: v${{ github.ref }} body_path: ${{ env.RELEASE_NOTES_PATH }} - # the package name should be with the format: AppFlowy--- - build-for-windows: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) needs: create-release env: WINDOWS_APP_RELEASE_PATH: frontend\appflowy_flutter\product\${{ github.ref_name }}\windows - WINDOWS_ZIP_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64.zip - WINDOWS_INSTALLER_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64 + WINDOWS_ZIP_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64.zip + WINDOWS_INSTALLER_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64 runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -52,13 +50,14 @@ jobs: - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Install flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -69,21 +68,23 @@ jobs: components: rustfmt profile: minimal + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: appflowy-lib-cache + key: ${{ matrix.job.os }}-${{ matrix.job.target }} + - name: Install prerequisites 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 - # the cargo make script has to be run separately because of file locking issues run: | flutter config --enable-windows-desktop - dart ./scripts/flutter_release_build/build_flowy.dart exclude-directives . ${{ github.ref_name }} cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-windows-x86 appflowy - dart ./scripts/flutter_release_build/build_flowy.dart include-directives . ${{ github.ref_name }} - name: Archive Asset uses: vimtor/action-zip@v1 @@ -123,28 +124,29 @@ jobs: asset_name: ${{ env.WINDOWS_INSTALLER_NAME }}.exe asset_content_type: application/octet-stream - build-for-macOS-x86_64: + build-for-macOS: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] runs-on: ${{ matrix.job.os }} needs: create-release env: MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release - MACOS_X86_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.zip - MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64 + MACOS_X86_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64.zip + MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64 strategy: 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 + uses: actions/checkout@v3 - name: Install flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -155,27 +157,22 @@ jobs: components: rustfmt profile: minimal + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: appflowy-lib-cache + key: ${{ matrix.job.os }}-${{ matrix.job.target }} + - 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 run: | flutter config --enable-macos-desktop - dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }} - - - name: Codesign AppFlowy - run: | - echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12 - security create-keychain -p action build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p action build.keychain - security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain - /usr/bin/codesign --force --options runtime --deep --sign "${{ secrets.MACOS_CODESIGN_ID }}" "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" -v + dart ./scripts/flutter_release_build/build_flowy.dart . ${{ github.ref_name }} - name: Create macOS dmg run: | @@ -191,10 +188,6 @@ jobs: "${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \ "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" - - name: Notarize AppFlowy - run: | - xcrun notarytool submit ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} -v -f "json" --wait - - name: Archive Asset working-directory: ${{ env.MACOS_APP_RELEASE_PATH }} run: zip --symlinks -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app @@ -219,118 +212,15 @@ jobs: asset_name: ${{ env.MACOS_DMG_NAME }}.dmg asset_content_type: application/octet-stream - build-for-macOS-universal: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] - runs-on: ${{ matrix.job.os }} - needs: create-release - env: - MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release - MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.zip - MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal - strategy: - fail-fast: false - matrix: - job: - - { - targets: "aarch64-apple-darwin,x86_64-apple-darwin", - os: macos-latest, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - targets: ${{ matrix.job.targets }} - components: rustfmt - - - name: Install prerequisites - working-directory: frontend - run: | - cargo install --force --locked cargo-make - cargo install --force --locked duckscript_cli - - - name: Build AppFlowy - working-directory: frontend - run: | - flutter config --enable-macos-desktop - sh scripts/flutter_release_build/build_universal_package_for_macos.sh ${{ github.ref_name }} - - - name: Codesign AppFlowy - run: | - echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12 - security create-keychain -p action build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p action build.keychain - security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain - /usr/bin/codesign --force --options runtime --deep --sign "${{ secrets.MACOS_CODESIGN_ID }}" "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" -v - - - name: Create macOS dmg - run: | - brew install create-dmg - create-dmg \ - --volname ${{ env.MACOS_DMG_NAME }} \ - --hide-extension "AppFlowy.app" \ - --background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \ - --window-size 600 450 \ - --icon-size 94 \ - --icon "AppFlowy.app" 141 249 \ - --app-drop-link 458 249 \ - "${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \ - "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" - - - name: Notarize AppFlowy - run: | - xcrun notarytool submit ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} -v -f "json" --wait - - - name: Archive Asset - working-directory: ${{ env.MACOS_APP_RELEASE_PATH }} - run: zip --symlinks -qr ${{ env.MACOS_AARCH64_ZIP_NAME }} AppFlowy.app - - - name: Upload Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_AARCH64_ZIP_NAME }} - asset_name: ${{ env.MACOS_AARCH64_ZIP_NAME }} - asset_content_type: application/octet-stream - - - name: Upload DMG Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg - asset_name: ${{ env.MACOS_DMG_NAME }}.dmg - asset_content_type: application/octet-stream - build-for-linux: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] runs-on: ${{ matrix.job.os }} needs: create-release env: LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release - LINUX_ZIP_NAME: AppFlowy-${{ matrix.job.target }}-x86_64.tar.gz - LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb - LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm - LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm - LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage - LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage - LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz - + LINUX_ZIP_NAME: AppFlowy_${{ matrix.job.target }}_${{ matrix.job.os }}.tar.gz + LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_${{ matrix.job.os }}.deb + # PKG_CONFIG_SYSROOT_DIR: / strategy: fail-fast: false matrix: @@ -338,19 +228,20 @@ jobs: - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-22.04, + os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-x86_64, } steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Install flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -361,6 +252,11 @@ jobs: components: rustfmt profile: minimal + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: appflowy-lib-cache + key: ${{ matrix.job.os }}-${{ matrix.job.target }} + - name: Install prerequisites working-directory: frontend run: | @@ -368,46 +264,50 @@ 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 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 if: ${{ matrix.job.target == 'aarch64-unknown-linux-gnu' }} working-directory: frontend run: | - sudo apt-get install -qy binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libgtk-3-0 + sudo apt-get install -qy binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - name: Build AppFlowy working-directory: frontend run: | flutter config --enable-linux-desktop - dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }} + dart ./scripts/flutter_release_build/build_flowy.dart . ${{ github.ref_name }} - - name: Archive Asset + - name: Archive Assert working-directory: ${{ env.LINUX_APP_RELEASE_PATH }} run: tar -czf ${{ env.LINUX_ZIP_NAME }} * - - name: Build Linux package (.deb) + - name: Configuring Linux Package working-directory: frontend run: | - sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_DEB_NAME }} + mkdir -p ../${{ env.LINUX_APP_RELEASE_PATH }}/package/opt + mkdir -p ../${{ env.LINUX_APP_RELEASE_PATH }}/package/usr/share/applications + cp -r ./scripts/linux_installer ../${{ env.LINUX_APP_RELEASE_PATH }}/package/DEBIAN + cd ../${{ env.LINUX_APP_RELEASE_PATH }}/package/DEBIAN + grep -rl "\[CHANGE_THIS\]" ./control | xargs sed -i "s/\[CHANGE_THIS\]/${{ github.ref_name }}/" + chmod 0755 {postinst,postrm} - - name: Build Linux package (.rpm) + - name: Build Linux package working-directory: ${{ env.LINUX_APP_RELEASE_PATH }} run: | - sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }} - cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }} + mv AppFlowy package/opt/ + cd package - - name: Build Linux package (.AppImage) - working-directory: frontend - continue-on-error: true - run: | - sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }} - cd .. - cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }} + # Update Exec & icon path in desktop entry + grep -rl "\[CHANGE_THIS\]" ./opt/AppFlowy/appflowy.desktop.temp | xargs sed -i "s/\[CHANGE_THIS\]/\/opt/" + # Add desktop entry in package + mv ./opt/AppFlowy/appflowy.desktop.temp ./usr/share/applications/appflowy.desktop + + # Build + cd ../ && dpkg-deb --build --root-owner-group -Z xz package ${{ env.LINUX_PACKAGE_NAME }} - name: Upload Asset id: upload-release-asset @@ -417,59 +317,38 @@ jobs: with: upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }} - asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }} + asset_name: ${{ env.LINUX_ZIP_NAME }} asset_content_type: application/octet-stream - - name: Upload Debian package - id: upload-release-asset-install-package-deb + - name: Upload Asset Install Package + id: upload-release-asset-install-package uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_DEB_NAME }} - asset_name: ${{ env.LINUX_PACKAGE_DEB_NAME }} - asset_content_type: application/octet-stream + asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_NAME }} - - name: Upload RPM package - id: upload-release-asset-install-package-rpm - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_RPM_NAME }} - asset_name: ${{ env.LINUX_PACKAGE_RPM_NAME }} - asset_content_type: application/octet-stream - - - name: Upload AppImage package - id: upload-release-asset-install-package-appimage - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }} - asset_name: ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }} + asset_name: ${{ env.LINUX_PACKAGE_NAME }} asset_content_type: application/octet-stream build-for-docker: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v2 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v1 - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v2 with: context: . file: ./frontend/scripts/docker-buildfiles/Dockerfile @@ -479,33 +358,9 @@ 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: - [ - build-for-linux, - build-for-windows, - build-for-macOS-x86_64, - build-for-macOS-universal, - ] + needs: [build-for-linux, build-for-windows, build-for-macOS] steps: - name: Notify Discord run: | diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 36c2e82064..c4daa54504 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -8,121 +8,83 @@ on: - "release/*" paths: - "frontend/rust-lib/**" - - ".github/workflows/rust_ci.yaml" + - "shared-lib/**" pull_request: branches: - "main" - "develop" - "release/*" + paths: + - "frontend/rust-lib/**" + - "shared-lib/**" env: CARGO_TERM_COLOR: always - CLOUD_VERSION: 0.8.3-amd64 - RUST_TOOLCHAIN: "1.81.0" + RUST_TOOLCHAIN: "1.70" + FLUTTER_VERSION: "3.10.1" 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 - 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 - - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v2 - 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 flutter + id: flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - 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 + frontend/rust-lib - - name: Checkout appflowy cloud code - uses: actions/checkout@v4 - with: - repository: AppFlowy-IO/AppFlowy-Cloud - path: AppFlowy-Cloud - - - name: Prepare appflowy cloud env - working-directory: AppFlowy-Cloud + - name: Build FlowySDK + working-directory: frontend run: | - 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 - working-directory: AppFlowy-Cloud - env: - APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }} - 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 compose up -d - echo "Waiting for the container to be ready..." - sleep 10 - docker ps -a - docker compose logs + cargo make --profile development-linux-x86_64 appflowy-core-dev - name: Run rust-lib tests working-directory: frontend/rust-lib - env: - RUST_LOG: info - RUST_BACKTRACE: 1 - af_cloud_test_base_url: http://localhost - af_cloud_test_ws_url: ws://localhost/ws/v1 - af_cloud_test_gotrue_url: http://localhost/gotrue - run: | - DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart" + run: RUST_LOG=info RUST_BACKTRACE=1 cargo test --no-default-features --features="rev-sqlite" - name: rustfmt rust-lib run: cargo fmt --all -- --check working-directory: frontend/rust-lib/ - name: clippy rust-lib - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --features="rev-sqlite" 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: rustfmt shared-lib + run: cargo fmt --all -- --check + working-directory: shared-lib - - name: Clean up Docker images - run: | - docker image prune -af - docker volume prune -f + - name: clippy shared-lib + run: cargo clippy -- -D warnings + working-directory: shared-lib + + - name: Run shared-lib tests + working-directory: shared-lib + run: RUST_LOG=info cargo test --no-default-features diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index 53a5f66748..2121fcf6ae 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -7,18 +7,19 @@ on: - "release/*" paths: - "frontend/rust-lib/**" + - "shared-lib/**" env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.27.4" - RUST_TOOLCHAIN: "1.81.0" + FLUTTER_VERSION: "3.10.1" + RUST_TOOLCHAIN: "1.70" jobs: tests: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v2 - name: Install Rust toolchain id: rust_toolchain @@ -40,8 +41,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/tauri_ci.yaml b/.github/workflows/tauri_ci.yaml new file mode 100644 index 0000000000..a69df60ad5 --- /dev/null +++ b/.github/workflows/tauri_ci.yaml @@ -0,0 +1,83 @@ +name: Tauri-CI +on: + pull_request: + paths: + - "frontend/rust-lib/**" + - "frontend/appflowy_tauri/**" + +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" + RUST_TOOLCHAIN: "1.70" + +jobs: + tauri-build: + strategy: + fail-fast: false + matrix: +# platform: [macos-latest, ubuntu-latest, windows-latest] + platform: [ubuntu-latest] + + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v3 + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + profile: minimal + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: 'ubuntu-latest-tauri' + workspaces: | + frontend/rust-lib + frontend/appflowy_tauri/src-tauri + + - name: install dependencies (windows only) + if: matrix.platform == 'windows-latest' + working-directory: frontend + run: | + cargo install --force cargo-make + cargo install --force duckscript_cli + vcpkg integrate install + cargo make appflowy-tauri-deps-tools + npm install -g pnpm@${{ env.PNPM_VERSION }} + + - name: install dependencies (ubuntu only) + if: matrix.platform == 'ubuntu-latest' + 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 + cargo install --force cargo-make + cargo make appflowy-tauri-deps-tools + npm install -g pnpm@${{ env.PNPM_VERSION }} + + - name: install dependencies (macOS only) + if: matrix.platform == 'macos-latest' + working-directory: frontend + run: | + cargo install --force cargo-make + cargo make appflowy-tauri-deps-tools + npm install -g pnpm@${{ env.PNPM_VERSION }} + + + - name: build + working-directory: frontend/appflowy_tauri + run: | + mkdir dist + pnpm install + cargo make --cwd .. tauri_build + pnpm test:errors + + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 33de28002a..bce47974c4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,11 @@ node_modules **/resources/proto -# ignore settings.json -frontend/.vscode/settings.json +!frontend/.vscode/settings.json +!frontend/.vscode/tasks.json +!frontend/.vscode/launch.json +!frontend/.vscode/extensions.json +!frontend/.vscode/*.code-snippets # Commit the highest level pubspec.lock, but ignore the others pubspec.lock @@ -34,11 +37,3 @@ pubspec.lock .fvm/ **/AppFlowy-Collab/ - -# ignore generated assets -frontend/package -frontend/*.deb - -**/Cargo.toml.bak - -**/.cargo/** \ No newline at end of file diff --git a/.run/ProtoBuf_Gen.run.xml b/.run/ProtoBuf_Gen.run.xml new file mode 100644 index 0000000000..13d9202183 --- /dev/null +++ b/.run/ProtoBuf_Gen.run.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/.run/Run backend.run.xml b/.run/Run backend.run.xml new file mode 100644 index 0000000000..840a367809 --- /dev/null +++ b/.run/Run backend.run.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/.run/dart-event.run.xml b/.run/dart-event.run.xml new file mode 100644 index 0000000000..c64ee9bf57 --- /dev/null +++ b/.run/dart-event.run.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e7e268a5..30b1c5b515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,803 +1,27 @@ # 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. -### Bug Fixes -- Resolved shortcut conflicts on the board page. -- Resolved an issue where underscores could cause the editor to freeze. - -## Version 0.5.9 - 06/06/2024 -### New Features -- Revamped the sidebar for both Desktop and Mobile. -- Added support for embedding videos in documents. -- Introduced a hotkey (Cmd/Ctrl + 0) to reset the app scale. -- Supported searching the workspace by page title. -### Bug Fixes -- Fixed the issue preventing the use of Backspace to delete words in Kanban boards. - -## Version 0.5.8 - 05/20/2024 -### New Features -- Improvement to the Callout block to insert new lines -- New settings page "Manage data" replaced the "Files" page -- New settings page "Workspace" replaced the "Appearance" and "Language" pages -- A custom implementation of a title bar for Windows users -- Added support for selecting Cards in kanban and performing grouped keyboard shortcuts -- Added support for default system font family -- Support for scaling the application up/down using a keyboard shortcut (CMD/CTRL + PLUS/MINUS) - -### Bug Fixes -- Resolved and refined the UI on Mobile -- Resolved issue with text editing in database -- Improved appearance of empty text cells in kanban/calendar -- Resolved an issue where a page's more actions (delete, duplicate) did not work properly -- Resolved and inconsistency in padding on get started screen on Desktop - -## Version 0.5.7 - 05/10/2024 -### Bug Fixes -- Resolved page opening issue on Android. -- Fixed text input inconsistency on Kanban board cards. - -## Version 0.5.6 - 05/07/2024 -### New Features -- Team collaboration is live! Add members to your workspace to edit and collaborate on pages together. -- Collaborate in real time on the same page with other members. Edits made by others will appear instantly. -- Create multiple workspaces for different kinds of content. -- Customize your entire page on mobile through the Page Style menu with options for layout, font, font size, emoji, and cover image. -- Open a row record as a full page. -### Bug Fixes -- Resolved issue with setting background color for the Simple Table block. -- Adjusted toolbar for various screen sizes. -- Added a request for photo permission before uploading images on mobile. -- Exported creation and last modification timestamps to CSV. - -## Version 0.5.5 - 04/24/2024 -### New Features -- Improved the display of code blocks with line numbers -- Added support for signing in using Magic Link -### Bug Fixes -- Fixed the database synchronization indicator issue -- Resolved the issue with opening the mentioned page on mobile -- Cleared the collaboration status when the user exits AppFlowy - -## Version 0.5.4 - 04/08/2024 -### New Features -- Introduced support for displaying a synchronization indicator within documents and databases to enhance user awareness of data sync status -- Revamped the select option cell editor in database -- Improved translations for Spanish, German, Kurdish, and Vietnamese -- Supported Android 6 and newer versions -### Bug Fixes -- Resolved an issue where twelve-hour time formats were not being parsed correctly in databases -- Fixed a bug affecting the user interface of the single select option filter -- Fixed various minor UI issues - -## Version 0.5.3 - 03/21/2024 -### New Features -- Added build support for 32-bit Android devices -- Introduced filters for KanBan boards for enhanced organization -- Introduced the new "Relations" column type in Grids -- Expanded language support with the addition of Greek -- Enhanced toolbar design for Mobile devices -- Introduced a command palette feature with initial support for page search -### Bug Fixes -- Rectified the issue of incomplete row data in Grids when adding new rows with active filters -- Enhanced the logic governing the filtering of number and select/multi-select fields for improved accuracy -- Implemented UI refinements on both Desktop and Mobile platforms, enriching the overall user experience of AppFlowy - -## Version 0.5.2 - 03/13/2024 -### Bug Fixes -- Import csv file. - -## Version 0.5.1 - 03/11/2024 -### New Features -- Introduced support for performing generic calculations on databases. -- Implemented functionality for easily duplicating calendar events. -- Added the ability to duplicate fields with cell data, facilitating smoother data management. -- Now supports customizing font styles and colors prior to typing. -- Enhanced the checklist user experience with the integration of keyboard shortcuts. -- Improved the dark mode experience on mobile devices. -### Bug Fixes -- Fixed an issue with some pages failing to sync properly. -- Fixed an issue where links without the http(s) scheme could not be opened, ensuring consistent link functionality. -- Fixed an issue that prevented numbers from being inserted before heading blocks. -- Fixed the inline page reference update mechanism to accurately reflect workspace changes. -- Fixed an issue that made it difficult to resize images in certain cases. -- Enhanced image loading reliability by clearing the image cache when images fail to load. -- Resolved a problem preventing the launching of URLs on some Linux distributions. - -## Version 0.5.0 - 02/26/2024 -### New Features -- Added support for scaling text on mobile platforms for better readability. -- Introduced a toggle for favorites directly from the documents' top bar. -- Optimized the image upload process and added error messaging for failed uploads. -- Implemented depth control for outline block components. -- New checklist task creation is now more intuitive, with prompts appearing on hover over list items in the row detail page. -- Enhanced sorting capabilities, allowing reordering and addition of multiple sorts. -- Expanded sorting and filtering options to include more field types like checklist, creation time, and modification time. -- Added support for field calculations within databases. -### Bug Fixes -- Fixed an issue where inserting an image from Unsplash in local mode was not possible. -- Fixed undo/redo functionality in lists. -- Fixed data loss issues when converting between block types. -- Fixed a bug where newly created rows were not being automatically sorted. -- Fixed issues related to deleting a sorting field or sort not removing existing sorts properly. -### Notes -- Windows 7, Windows 8, and iOS 11 are not yet supported due to the upgrade to Flutter 3.22.0. - -## Version 0.4.9 - 02/17/2024 -### Bug Fixes -- Resolved the issue that caused users to be redirected to the Sign In page - -## Version 0.4.8 - 02/13/2024 -### Bug Fixes -- Fixed a possible error when loading workspaces - -## Version 0.4.6 - 02/03/2024 -### Bug Fixes -- Fixed refresh token bug - -## Version 0.4.5 - 02/01/2024 -### Bug Fixes -- Fixed WebSocket connection issue - -## Version 0.4.4 - 01/31/2024 -### New Features -- Added functionality for uploading images to cloud storage. -- Enabled anonymous sign-in option for mobile platform users. -- Introduced the ability to customize cloud settings directly from the startup page. -- Added support for inserting reminders on the mobile platform. -- Overhauled the user interface on mobile devices, including improvements to the action bottom sheet, editor toolbar, database details page, and app bar. -- Implemented a shortcut (F2 key) to rename the current view. - -### Bug Fixes -- Fixed an issue where the font family was not displaying correctly on the mobile platform. -- Resolved a problem with the mobile row detail title not updating correctly. -- Fixed issues related to deleting images and refactored the image actions menu for better usability. -- Fixed other known issues. - -# Release Notes -## Version 0.4.3 - 01/16/2024 -### Bug Fixes -- Fixed file name too long issue - -## Version 0.4.2 - 01/15/2024 -AppFlowy for Android is available to download on GitHub. -If you’ve been using our desktop app, it’s important to read [this guide](https://docs.appflowy.io/docs/guides/sync-desktop-and-mobile) before logging into the mobile app. -### New Features -- Enhanced RTL (Right-to-Left) support for mobile platforms. -- Optimized selection gesture system on mobile. -- Optimized the mobile toolbar menu. -- Improved reference menu (‘@’ menu). -- Updated privacy policy. -- Improved the data import process for AppFlowy by implementing a progress indicator and compressing the data to enhance efficiency. -- Enhanced the utilization of local disk space to optimize storage consumption. -### Bug Fixes -- Fixed sign-in cancellation issue on mobile. -- Resolved keyboard close bug on Android. - - -## Version 0.4.1 - 01/03/2024 -### Bug fixes -- Fix import AppFlowy data folder - -## Version 0.4.0 - 12/30/2023 -1. Added capability to import data from an AppFlowy data folder. For detailed information, please see [AppFlowy Data Storage Documentation](https://docs.appflowy.io/docs/appflowy/product/data-storage). -2. Enhanced user interface and fixed various bugs. -3. Improved the efficiency of data synchronization in AppFlowy Cloud - -## Version 0.3.9.1 - 12/07/2023 - -### Bug fixes -- Fix potential blank pages that may occur in an empty document - -## Version 0.3.9 - 12/07/2023 - -### New Features -- Support inserting a new field to the left or right of an existing one - -### Bug fixes -- Fix some emojis are shown in black/white -- Fix unable to rename a subpage of subpage - -## Version 0.3.8 - 11/13/2023 - -### New Features -- Support hiding any stack in a board -- Support customizing page icons in menu -- Display visual hint when card contains notes -- Quick action for adding new stack to a board -- Support more ways of inserting page references in documents -- Shift + click on a checkbox to power toggle its children - -### Bug fixes -- Improved color of the "Share"-button text -- Text overflow issue in Calendar properties -- Default font (Roboto) added to application -- Placeholder added for the editor inside a Card -- Toggle notifications in settings have been fixed -- Dialog for linking board/grid/calendar opens in correct position -- Quick add Card in Board at top, correctly adds a new Card at the top - -## Version 0.3.7 - 10/30/2023 - -### New Features -- Support showing checklist items inline in row page. -- Support inserting date from slash menu. -- Support renaming a stack directly by clicking on the stack name. -- Show the detailed reminder content in the notification center. -- Save card order in Board view. -- Allow to hide the ungrouped stack. -- Segmented the checklist progress bar. - -### Bug fixes -- Optimize side panel animation. -- Fix calendar with hidden date or title doesn't show options correctly. -- Fix the horizontal scroll bar disappears in Grid view. -- Improve setting tab UI in Grid view. -- Improve theme of the code block. -- Fix some UI issues. - -## Version 0.3.6 - 10/16/2023 - -### New Features -- Support setting Markdown styles through keyboard shortcuts. -- Added Ukrainian language. -- Support auto-hiding sidebar feature, ensuring a streamlined view even when resizing to a smaller window. -- Support toggling the notifitcation on/off. -- Added Lemonade theme. - -### Bug fixes -- Improve Vietnamese translations. -- Improve reminder feature. -- Fix some UI issues. - -## Version 0.3.5 - 10/09/2023 - -### New Features -- Added support for browsing and inserting images from Unsplash. -- Revamp and unify the emoji picker throughout AppFlowy. - -### Bug fixes -- Improve layout of the settings page. -- Improve design of the restore page banner. -- Improve UX of the reminders. -- Other UI fixes. - -## Version 0.3.4 - 10/02/2023 - -### New Features -- Added support for creating a reminder. -- Added support for finding and replacing in the document page. -- Added support for showing the hidden fields in row detail page. -- Adjust the toolbar style in RTL mode. - -### Bug fixes -- Improve snackbar UI design. -- Improve dandelion theme. -- Improve id-ID and pl-PL language translations. - -## Version 0.3.3 - 09/24/2023 - -### New Features -- Added an end date field to the time cell in the database. -- Added Support for customizing the font family from GoogleFonts in the editor. -- Set the uploaded image to cover by default. -- Added Support for resetting the user icon on settings page -- Add Urdu language translations. - -### Bug fixes -- Default colors for the blocks except for the callout were not transparent. -- Option/Alt + click to add a block above didn't work on the first line. -- Unable to paste HTML content containing `` tag. -- Unable to select the text from anywhere in the line. -- The selection in the editor didn't clear when editing the inline database. -- Added a bottom border to new property column in the database. -- Set minimum width of 50px for grid fields. - -## Version 0.3.2 - 09/18/2023 - -### New Features - -- Improve the performance of the editor, now it is much faster when editing a large document. -- Support for reordering the rows of the database on Windows. -- Revamp the row detail page of the database. -- Revamp the checklist cell editor of the database. - -### Bug fixes - -- Some UI issues - -## Version 0.3.1 - 09/04/2023 - -### New Features - -- Improve CJK (Chinese, Japanese, Korean) input method support. -- Share a database in CSV format. -- Support for aligning the block component with the toolbar. -- Support for editing name when creating a new page. -- Support for inserting a table in the document page. -- Database views allow for independent field visibility toggling. - -### Bug fixes - -- Paste multiple lines in code block. -- Some UI issues - -## Version 0.3.0 - 08/22/2023 - -### New Features - -- Improve paste features: - - Paste HTML content from website. - - Paste image from clipboard. - -- Support Group by Date in Kanban Board. -- Notarize the macOS package, which is now verified by Apple. -- Add Persian language translations. - -### Bug fixes - -- Some UI issues - -## Version 0.2.9 - 08/08/2023 - -### New Features - -- Improve tab and shortcut, click with alt/option to open a page in new tab. -- Improve database tab bar UI. - -### Bug fixes - -- Add button and more action button of the favorite section doesn't work. -- Fix euro currency number format. -- Some UI issues - -## Version 0.2.8 - 08/03/2023 - -### New Features - -- Nestable personal folder that supports drag and drop -- Support for favorite folders. -- Support for sorting by date in Grid view. -- Add a duplicate button in the Board context menu. - -### Bug fixes - -- Improve readability in Callout -- Some UI issues - -## Version 0.2.7 - 07/18/2023 - -### New Features - - - -- Open page in new tab -- Create toggle lists to keep things tidy in your pages -- Alt/Option + click to add a text block above - -### Bug fixes - -- Pasting into a Grid property crashed on Windows -- Double-click a link to open - -## Version 0.2.6 - 07/11/2023 - -### New Features - -- Dynamic load themes -- Inline math equation - - -## Version 0.2.5 - 07/02/2023 - -### New Features - -- Insert local images -- Mention a page -- Outlines (Table of contents) -- Added support for aligning the image by image menu - -### Bug fixes - -- Some UI issues - -## Version 0.2.4 - 06/23/2023 - -### Bug fixes: - -- Unable to copy and paste a word -- Some UI issues - ## Version 0.2.3 - 06/21/2023 - ### New Features - -- Added support for creating multiple database views for existing database +- Added support for creating multiple database views for existing database ## Version 0.2.2 - 06/15/2023 - ### New Features - - Added support for embedding a document in the database's row detail page - Added support for inserting an emoji in the database's row detail page ### Other Updates - - Added language selector on the welcome page - Added support for importing multiple markdown files all at once ## Version 0.2.1 - 06/11/2023 - ### New Features - - Added support for creating or referencing a calendar in the document - Added `+` icon in grid's add field ### Other Updates - - Added vertical padding for progress bar - Hide url cell accessory when the content is empty ### Bug fixes: - - Fixed unable to export markdown - Fixed adding vertical padding for progress bar - Fixed database view didn't update after the database layout changed. @@ -805,7 +29,6 @@ If you’ve been using our desktop app, it’s important to read [this guide](ht ## Version 0.2.0 - 06/08/2023 ### New Features - - Improved checklists to support each cell having its own list - Drag and drop calendar events - Switch layouts (calendar, grid, kanban) of a database @@ -819,53 +42,44 @@ If you’ve been using our desktop app, it’s important to read [this guide](ht - Added support for an 'Option' button to delete, duplicate, and customize block actions ### Other Updates - - Added support for importing v0.1.x documents and databases - Added support for database import and export to CSV - Optimized scroll behavior in documents. - Redesigned the launch page ### Bug fixes - - Fixed bugs related to numbers - Fixed issues with referenced databases in documents - Fixed menu overflow issues in documents ### Data migration - The data format of this version is not compatible with previous versions. Therefore, to migrate your data to the new version, you need to use the export and import functions. Please follow the guide to learn how to export and import your data. #### Export files in v0.1.6 - https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9 #### Import files in v0.2.0 - https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2 ## Version 0.1.5 - 11/05/2023 ### Bug Fixes - - Fix: calendar dates don't match with weekdays. - Fix: sort numbers in Grid. ## Version 0.1.4 - 04/05/2023 ### New features - - Use AppFlowy’s calendar views to plan and manage tasks and deadlines. - Writing can be improved with the help of OpenAI. ## Version 0.1.3 - 24/04/2023 ### New features - - Launch the official Dark Mode. - Customize the font color and highlight color by setting a hex color value and an opacity level. ### Bug Fixes - - Fix: the slash menu can be triggered by all other keyboards than English. - Fix: convert the single asterisk to italic text and the double asterisks to bold text. @@ -1117,4 +331,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..2245165be5 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,44 +18,28 @@ 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) -- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source) +* [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages) +* [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker) +* [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source) ## Built With -- [Flutter](https://flutter.dev/) +* [Flutter](https://flutter.dev/) -- [Rust](https://www.rust-lang.org/) +* [Rust](https://www.rust-lang.org/) ## Stay Up-to-Date @@ -63,41 +47,26 @@ 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://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) 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) +* [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/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://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/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. - -## 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. +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. ## Join the community to build AppFlowy together @@ -107,51 +76,33 @@ 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 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. We decided to achieve this mission by upholding the three most fundamental values: -- Data privacy first -- Reliable native experience -- Community-driven extensibility +* Data privacy first +* 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: -- [cargo-make](https://github.com/sagiegurari/cargo-make) -- [contrib.rocks](https://contrib.rocks) -- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui) +* [flutter-quill](https://github.com/singerdmx/flutter-quill) +* [cargo-make](https://github.com/sagiegurari/cargo-make) +* [contrib.rocks](https://contrib.rocks) 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..cd131cf179 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -1,125 +1,113 @@ { - // 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 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 only builds the Dart code of AppFlowy. + "name": "AF-desktop: Build Dart Only", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "env": { + "RUST_LOG": "debug", + }, + "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-desktop: Debug Rust", + "request": "attach", + "type": "lldb", + "pid": "${command:pickMyProcess}" + }, + // { + // "name": "AF-desktop: profile mode", + // "request": "launch", + // "program": "./lib/main.dart", + // "type": "dart", + // "flutterMode": "profile", + // "cwd": "${workspaceRoot}/appflowy_flutter" + // }, + { + // This task builds the Rust and Dart code of AppFlowy for android. + "name": "AF-android: Build All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: build_mobile_sdk", + "env": { + "RUST_LOG": "info" + }, + "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-android: Clean + Rebuild All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Clean + Rebuild All (Android)", + "env": { + "RUST_LOG": "info" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + // 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/" + }, + // { + // "type": "lldb", + // "request": "launch", + // "name": "AF-tauri: Production Debug", + // "cargo": { + // "args": ["build", "--release", "--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml"] + // }, + // "preLaunchTask": "AF: Tauri UI Build", + // "cwd": "${workspaceRoot}/appflowy_tauri/" + // }, + ] +} \ No newline at end of file diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 0000000000..f7d0c23058 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,36 @@ +{ + "[dart]": { + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.rulers": [80], + "editor.selectionHighlight": false, + "editor.suggest.snippetsPreventQuickSuggestions": false, + "editor.suggestSelection": "first", + "editor.tabCompletion": "onlySnippets", + "editor.wordBasedSuggestions": false, + }, + "[javascript]": { + "editor.formatOnSave": true, + "editor.rulers": [80], + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.formatOnSave": true, + "editor.rulers": [80], + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "svgviewer.enableautopreview": true, + "svgviewer.previewcolumn": "Active", + "svgviewer.showzoominout": true, + "editor.wordWrapColumn": 80, + "editor.minimap.maxColumn": 140, + "editor.wordWrap": "wordWrapColumn", + "dart.lineLength": 80, + "typescript.validate.enable": true, + "javascript.validate.enable": true, + "files.associations": { + "*.log.*": "log" + }, + "editor.formatOnSave": true, + "files.eol": "\n", +} \ No newline at end of file diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index 0be167fb12..cec5290496 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -18,45 +18,9 @@ "AF: Flutter Clean", "AF: Build Appflowy Core", "AF: Flutter Pub Get", + "AF: Flutter Package Get", "AF: Generate Language Files", - "AF: Generate Freezed Files", - "AF: Generate Svg Files" - ], - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "AF: Clean + Rebuild All (iOS)", - "type": "shell", - "dependsOrder": "sequence", - "dependsOn": [ - "AF: Dart Clean", - "AF: Flutter Clean", - "AF: Build Appflowy Core For iOS", - "AF: Flutter Pub Get", - "AF: Generate Language Files", - "AF: Generate Freezed Files", - "AF: Generate Svg Files" - ], - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "AF: Clean + Rebuild All (iOS Simulator)", - "type": "shell", - "dependsOrder": "sequence", - "dependsOn": [ - "AF: Dart Clean", - "AF: Flutter Clean", - "AF: Build Appflowy Core For iOS Simulator", - "AF: Flutter Pub Get", - "AF: Generate Language Files", - "AF: Generate Freezed Files", - "AF: Generate Svg Files" + "AF: Generate Freezed Files" ], "presentation": { "reveal": "always", @@ -70,17 +34,26 @@ "dependsOn": [ "AF: Dart Clean", "AF: Flutter Clean", - "AF: Build Appflowy Core For Android", + "AF: Build Appflowy Core_for_android", "AF: Flutter Pub Get", + "AF: Flutter Package Get", "AF: Generate Language Files", - "AF: Generate Freezed Files", - "AF: Generate Svg Files" + "AF: Generate Freezed Files" ], "presentation": { "reveal": "always", "panel": "new" } }, + { + "label": "AF: Build Appflowy Core_for_android", + "type": "shell", + "command": "cargo make --profile development-android appflowy-core-dev-android", + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + } + }, { "label": "AF: Build Appflowy Core", "type": "shell", @@ -98,33 +71,6 @@ "cwd": "${workspaceFolder}" } }, - { - "label": "AF: Build Appflowy Core For iOS", - "type": "shell", - "command": "cargo make --profile development-ios-arm64 appflowy-core-dev-ios", - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: Build Appflowy Core For iOS Simulator", - "type": "shell", - "command": "cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios", - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, - { - "label": "AF: Build Appflowy Core For Android", - "type": "shell", - "command": "cargo make --profile development-android appflowy-core-dev-android", - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, { "label": "AF: Code Gen", "type": "shell", @@ -132,9 +78,9 @@ "dependsOn": [ "AF: Flutter Clean", "AF: Flutter Pub Get", + "AF: Flutter Package Get", "AF: Generate Language Files", - "AF: Generate Freezed Files", - "AF: Generate Svg Files" + "AF: Generate Freezed Files" ], "group": { "kind": "build", @@ -145,15 +91,6 @@ "panel": "new" } }, - { - "label": "AF: Dart Clean", - "type": "shell", - "command": "cargo make flutter_clean", - "group": "build", - "options": { - "cwd": "${workspaceFolder}" - } - }, { "label": "AF: Flutter Clean", "type": "shell", @@ -170,31 +107,26 @@ "cwd": "${workspaceFolder}/appflowy_flutter" } }, + { + "label": "AF: Flutter Package Get", + "type": "shell", + "command": "flutter packages pub get", + "options": { + "cwd": "${workspaceFolder}/appflowy_flutter" + } + }, { "label": "AF: Generate Freezed Files", "type": "shell", - "command": "sh ./scripts/code_generation/freezed/generate_freezed.sh", + "command": "dart run build_runner build -d", "options": { - "cwd": "${workspaceFolder}" - }, - "group": "build", - "windows": { - "options": { - "shell": { - "executable": "cmd.exe", - "args": [ - "/d", - "/c", - ".\\scripts\\code_generation\\freezed\\generate_freezed.cmd" - ] - } - } + "cwd": "${workspaceFolder}/appflowy_flutter" } }, { "label": "AF: Generate Language Files", "type": "shell", - "command": "sh ./scripts/code_generation/language_files/generate_language_files.sh", + "command": "sh ./scripts/generate_language_files.sh", "windows": { "options": { "shell": { @@ -202,7 +134,7 @@ "args": [ "/d", "/c", - ".\\scripts\\code_generation\\language_files\\generate_language_files.cmd" + ".\\scripts\\generate_language_files.cmd" ] } } @@ -213,21 +145,9 @@ } }, { - "label": "AF: Generate Svg Files", + "label": "AF: Flutter Clean", "type": "shell", - "command": "sh ./scripts/code_generation/flowy_icons/generate_flowy_icons.sh", - "windows": { - "options": { - "shell": { - "executable": "cmd.exe", - "args": [ - "/d", - "/c", - ".\\scripts\\code_generation\\flowy_icons\\generate_flowy_icons.cmd" - ] - } - } - }, + "command": "cargo make flutter_clean", "group": "build", "options": { "cwd": "${workspaceFolder}" @@ -246,12 +166,60 @@ "detail": "appflowy_flutter" }, { - "label": "AF: Generate Env File", + "label": "AF: Tauri UI Dev", "type": "shell", - "command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs", + "isBackground": true, + "command": "yarn", + "args": [ + "dev" + ], "options": { - "cwd": "${workspaceFolder}/appflowy_flutter" + "cwd": "${workspaceFolder}/appflowy_tauri" } - } + }, + { + "label": "AF: Tauri UI Build", + "type": "shell", + "command": "pnpm run build", + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, + { + "label": "AF: Tauri Dev", + "type": "shell", + "command": "npm run tauri: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" + } + }, ] -} \ No newline at end of file +} diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 41fdffb1af..f0e283695a 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -1,16 +1,14 @@ #https://github.com/sagiegurari/cargo-make extend = [ - { path = "scripts/makefile/desktop.toml" }, - { path = "scripts/makefile/mobile.toml" }, - { path = "scripts/makefile/protobuf.toml" }, - { path = "scripts/makefile/tests.toml" }, - { path = "scripts/makefile/docker.toml" }, - { path = "scripts/makefile/env.toml" }, - { path = "scripts/makefile/flutter.toml" }, - { path = "scripts/makefile/tool.toml" }, - { path = "scripts/makefile/tauri.toml" }, - { path = "scripts/makefile/web.toml" }, + { path = "scripts/makefile/desktop.toml" }, + { path = "scripts/makefile/protobuf.toml" }, + { path = "scripts/makefile/tests.toml" }, + { path = "scripts/makefile/docker.toml" }, + { path = "scripts/makefile/env.toml" }, + { path = "scripts/makefile/flutter.toml" }, + { path = "scripts/makefile/tool.toml" }, + { path = "scripts/makefile/tauri.toml" }, ] [config] @@ -21,15 +19,13 @@ run_task = { name = ["restore-crate-type"] } [env] RUST_LOG = "info" -CARGO_PROFILE = "dev" 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" -FLUTTER_DESKTOP_FEATURES = "dart" +CURRENT_APP_VERSION = "0.2.3" +FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" -MACOSX_DEPLOYMENT_TARGET = "11.0" # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html # If you update the macOS's CRATE_TYPE, don't forget to update the # appflowy_backend.podspec @@ -49,8 +45,6 @@ LIB_EXT = "a" 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" -TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend" # Test default config TEST_CRATE_TYPE = "cdylib" TEST_LIB_EXT = "dylib" @@ -65,7 +59,6 @@ BUILD_FLAG = "debug" FLUTTER_OUTPUT_DIR = "Debug" PRODUCT_EXT = "app" BUILD_ARCHS = "arm64" -BUILD_ACTIVE_ARCHS_ONLY = true CRATE_TYPE = "staticlib" [env.development-mac-x86_64] @@ -76,11 +69,9 @@ BUILD_FLAG = "debug" FLUTTER_OUTPUT_DIR = "Debug" PRODUCT_EXT = "app" BUILD_ARCHS = "x86_64" -BUILD_ACTIVE_ARCHS_ONLY = true CRATE_TYPE = "staticlib" [env.production-mac-arm64] -CARGO_PROFILE = "release" BUILD_FLAG = "release" TARGET_OS = "macos" RUST_COMPILE_TARGET = "aarch64-apple-darwin" @@ -88,11 +79,9 @@ FLUTTER_OUTPUT_DIR = "Release" PRODUCT_EXT = "app" APP_ENVIRONMENT = "production" BUILD_ARCHS = "arm64" -BUILD_ACTIVE_ARCHS_ONLY = false CRATE_TYPE = "staticlib" [env.production-mac-x86_64] -CARGO_PROFILE = "release" BUILD_FLAG = "release" TARGET_OS = "macos" RUST_COMPILE_TARGET = "x86_64-apple-darwin" @@ -100,18 +89,8 @@ FLUTTER_OUTPUT_DIR = "Release" PRODUCT_EXT = "app" APP_ENVIRONMENT = "production" BUILD_ARCHS = "x86_64" -BUILD_ACTIVE_ARCHS_ONLY = false CRATE_TYPE = "staticlib" -[env.production-mac-universal] -CARGO_PROFILE = "release" -BUILD_FLAG = "release" -TARGET_OS = "macos" -FLUTTER_OUTPUT_DIR = "Release" -PRODUCT_EXT = "app" -BUILD_ACTIVE_ARCHS_ONLY = false -APP_ENVIRONMENT = "production" - [env.development-windows-x86] TARGET_OS = "windows" RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc" @@ -122,7 +101,6 @@ CRATE_TYPE = "cdylib" LIB_EXT = "dll" [env.production-windows-x86] -CARGO_PROFILE = "release" BUILD_FLAG = "release" TARGET_OS = "windows" RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc" @@ -130,7 +108,6 @@ FLUTTER_OUTPUT_DIR = "Release" PRODUCT_EXT = "exe" CRATE_TYPE = "cdylib" LIB_EXT = "dll" -BUILD_ARCHS = "x64" APP_ENVIRONMENT = "production" [env.development-linux-x86_64] @@ -143,7 +120,6 @@ LIB_EXT = "so" LINUX_ARCH = "x64" [env.production-linux-x86_64] -CARGO_PROFILE = "release" BUILD_FLAG = "release" TARGET_OS = "linux" RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu" @@ -161,10 +137,9 @@ CRATE_TYPE = "cdylib" FLUTTER_OUTPUT_DIR = "Debug" LIB_EXT = "so" LINUX_ARCH = "arm64" -FLUTTER_DESKTOP_FEATURES = "dart,openssl_vendored" +FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored" [env.production-linux-aarch64] -CARGO_PROFILE = "release" BUILD_FLAG = "release" TARGET_OS = "linux" RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu" @@ -173,48 +148,7 @@ FLUTTER_OUTPUT_DIR = "Release" LIB_EXT = "so" LINUX_ARCH = "arm64" APP_ENVIRONMENT = "production" -FLUTTER_DESKTOP_FEATURES = "dart,openssl_vendored" - -[env.development-ios-arm64-sim] -BUILD_FLAG = "debug" -TARGET_OS = "ios" -FLUTTER_OUTPUT_DIR = "Debug" -RUST_COMPILE_TARGET = "aarch64-apple-ios-sim" -BUILD_ARCHS = "arm64" -CRATE_TYPE = "staticlib" - -[env.development-ios-arm64] -BUILD_FLAG = "debug" -TARGET_OS = "ios" -FLUTTER_OUTPUT_DIR = "Debug" -RUST_COMPILE_TARGET = "aarch64-apple-ios" -BUILD_ARCHS = "arm64" -CRATE_TYPE = "staticlib" - -[env.production-ios-arm64] -BUILD_FLAG = "release" -TARGET_OS = "ios" -FLUTTER_OUTPUT_DIR = "Release" -RUST_COMPILE_TARGET = "aarch64-apple-ios" -BUILD_ARCHS = "arm64" -CRATE_TYPE = "staticlib" - -[env.development-android] -BUILD_FLAG = "debug" -TARGET_OS = "android" -CRATE_TYPE = "cdylib" -FLUTTER_OUTPUT_DIR = "Debug" -LIB_EXT = "so" -PRODUCT_EXT = "apk" -FLUTTER_DESKTOP_FEATURES = "dart,openssl_vendored" - -[env.production-android] -BUILD_FLAG = "release" -TARGET_OS = "android" -CRATE_TYPE = "cdylib" -FLUTTER_OUTPUT_DIR = "Release" -PRODUCT_EXT = "apk" -LIB_EXT = "so" +FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored" [tasks.echo_env] script = [''' @@ -226,32 +160,52 @@ script = [''' echo FEATURES: ${FLUTTER_DESKTOP_FEATURES} echo PRODUCT_EXT: ${PRODUCT_EXT} echo APP_ENVIRONMENT: ${APP_ENVIRONMENT} - echo BUILD_ARCHS: ${BUILD_ARCHS} - echo BUILD_VERSION: ${BUILD_VERSION} + echo ${platforms} + echo ${BUILD_ARCHS} '''] script_runner = "@shell" +[env.production-ios] +BUILD_FLAG = "release" +TARGET_OS = "ios" +FLUTTER_OUTPUT_DIR = "Release" +PRODUCT_EXT = "ipa" + +[env.development-android] +BUILD_FLAG = "debug" +TARGET_OS = "android" +CRATE_TYPE = "cdylib" +FLUTTER_OUTPUT_DIR = "Debug" +FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored" + +[env.production-android] +BUILD_FLAG = "release" +TARGET_OS = "android" +CRATE_TYPE = "cdylib" +FLUTTER_OUTPUT_DIR = "Release" +FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored" + [tasks.setup-crate-type] private = true script = [ - """ - toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml - val = replace ${toml} "staticlib" ${CRATE_TYPE} - result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} - assert ${result} - """, + """ + toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml + val = replace ${toml} "staticlib" ${CRATE_TYPE} + result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} + assert ${result} + """, ] script_runner = "@duckscript" [tasks.restore-crate-type] private = true script = [ - """ - toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml - val = replace ${toml} ${CRATE_TYPE} "staticlib" - result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} - assert ${result} - """, + """ + toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml + val = replace ${toml} ${CRATE_TYPE} "staticlib" + result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} + assert ${result} + """, ] script_runner = "@duckscript" @@ -279,24 +233,24 @@ TEST_COMPILE_TARGET = "x86_64-pc-windows-msvc" [tasks.setup-test-crate-type] private = true script = [ - """ - toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml - val = replace ${toml} "staticlib" ${TEST_CRATE_TYPE} - result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} - assert ${result} - """, + """ + toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml + val = replace ${toml} "staticlib" ${TEST_CRATE_TYPE} + result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} + assert ${result} + """, ] script_runner = "@duckscript" [tasks.restore-test-crate-type] private = true script = [ - """ - toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml - val = replace ${toml} ${TEST_CRATE_TYPE} "staticlib" - result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} - assert ${result} - """, + """ + toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml + val = replace ${toml} ${TEST_CRATE_TYPE} "staticlib" + result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val} + assert ${result} + """, ] script_runner = "@duckscript" diff --git a/frontend/appflowy_flutter/.gitignore b/frontend/appflowy_flutter/.gitignore index 9c3bb46d62..504d82329b 100644 --- a/frontend/appflowy_flutter/.gitignore +++ b/frontend/appflowy_flutter/.gitignore @@ -69,12 +69,8 @@ windows/flutter/dart_ffi/ **/.sandbox **/.vscode/ -.env -.env.* +*.env coverage/ **/failures/*.png - -assets/translations/ -assets/flowy_icons/* \ No newline at end of file diff --git a/frontend/appflowy_flutter/.metadata b/frontend/appflowy_flutter/.metadata index 7da2cb55fd..9068867840 100644 --- a/frontend/appflowy_flutter/.metadata +++ b/frontend/appflowy_flutter/.metadata @@ -4,8 +4,8 @@ # This file should be version controlled. version: - revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 - channel: unknown + revision: 135454af32477f815a7525073027a3ff9eff1bfd + channel: stable project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 - base_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 - - platform: android - create_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 - base_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66 + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: windows + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd # User provided section diff --git a/frontend/appflowy_flutter/README.md b/frontend/appflowy_flutter/README.md index 116cd63f22..b4bd6916e1 100644 --- a/frontend/appflowy_flutter/README.md +++ b/frontend/appflowy_flutter/README.md @@ -1,7 +1,7 @@

AppFlowy_Flutter

- - + +
> Documentation for Contributors @@ -13,13 +13,11 @@ This Repository contains the codebase for the frontend of the application, curre - Linux - macOS - Windows - > We are actively working on support for Android & iOS! - -_Additionally, we are working on a Web version built with Tauri!_ + > We later expect to extend support to Android and iOS devices using Flutter. ### Am I Eligible to Contribute? -Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy). Some of the ways in which you can contribute are: +Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy). Some of the ways in which you can contribute are: - Non-Coding Contributions - Documentation @@ -28,23 +26,27 @@ Yes! You are eligible to contribute, check out the ways in which you can [contri - Improve Translations - Coding Contributions -To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we suggest you learn the basics, and then contribute afterwards. To get started with Flutter read [here](https://flutter.dev/docs/get-started/codelab). +To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we may suggest you to learn the basics and then try to contribute, get started with Flutter [here](https://flutter.dev/docs/get-started/codelab). -### What OS should I use for development? +### What OS Should I Use for Development? -We support all OS for Development i.e. Linux, MacOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://docs.appflowy.io/docs/documentation/appflowy/from-source/environment-setup) on how to setup `AppFlowy_Flutter` on your local system respectively per operating system. +We support all OS for Development i.e Linux, macOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/environment-setup) on How to Setup `AppFlowy_Flutter` in your local system in each OS. ### Getting Started ❇ -We have detailed documentation on how to [get started](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you: +We have a detailed documentation, on how to [get started](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you. - [Code Architecture](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/frontend/codemap) -- [Styleguide & Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions/naming-conventions) -- [Making Your First PR](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/submitting-your-first-pull-request) -- [All AppFlowy Documentation](https://docs.appflowy.io/docs/documentation/appflowy) - Contribution guide, build and run, debugging, testing, localization, etc. +- [Making Your First PR](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/submitting-your-first-pull-request) +- [The Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides) +- [How to run/debug the application](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/launcher-and-tasks) ### Need Help? -- New to GitHub? Follow [these](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/setting-up-your-repositories) steps to get started -- Stuck Somewhere? Join our [Discord](https://discord.gg/9Q2xaN37tV), we're there to help you! -- Find out more about the [community initiatives](https://docs.appflowy.io/docs/appflowy/community). +- New to GitHub? Follow [these](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/setting-up-your-repositories) steps to get started +- Stuck Somewhere? Join the [Discord](https://discord.gg/9Q2xaN37tV) Group and we are there to help you! + + diff --git a/frontend/appflowy_flutter/analysis_options.yaml b/frontend/appflowy_flutter/analysis_options.yaml index 4579b2d8c5..00ed18a2e6 100644 --- a/frontend/appflowy_flutter/analysis_options.yaml +++ b/frontend/appflowy_flutter/analysis_options.yaml @@ -1,35 +1,41 @@ +# 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 - - 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 +# 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/.gitignore b/frontend/appflowy_flutter/android/.gitignore index d34d43f48e..6f568019d3 100644 --- a/frontend/appflowy_flutter/android/.gitignore +++ b/frontend/appflowy_flutter/android/.gitignore @@ -11,5 +11,3 @@ GeneratedPluginRegistrant.java key.properties **/*.keystore **/*.jks - -.cxx diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle index 0b96e32472..948e9b7f42 100644 --- a/frontend/appflowy_flutter/android/app/build.gradle +++ b/frontend/appflowy_flutter/android/app/build.gradle @@ -25,14 +25,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" -def keystoreProperties = new Properties() -def keystorePropertiesFile = rootProject.file('key.properties') -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} - android { - compileSdkVersion 34 + compileSdkVersion 31 ndkVersion "24.0.8215888" compileOptions { @@ -52,48 +46,23 @@ 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 19 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true - externalNativeBuild { - cmake { - arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_STL=c++_shared" - } - } } - signingConfigs { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null - storePassword keystoreProperties['storePassword'] - } - } buildTypes { release { - // use release instead when publishing the application to google play. - // signingConfig signingConfigs.release + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + minifyEnabled true + shrinkResources true + signingConfig signingConfigs.debug } } - - namespace 'io.appflowy.appflowy' - - externalNativeBuild { - cmake { - path "src/main/CMakeLists.txt" - } - } - - // only support arm64-v8a - defaultConfig { - ndk { - abiFilters "arm64-v8a" - } - } } flutter { diff --git a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml index f880684a6a..7d5632662e 100644 --- a/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml @@ -1,4 +1,5 @@ - + diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index f746eeb610..264e1d3232 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -1,71 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt b/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt deleted file mode 100644 index 455c5081b6..0000000000 --- a/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -cmake_minimum_required(VERSION 3.10.0) - -project(AppFlowy) - -message(CONFIGURE_LOG "NDK PATH: ${ANDROID_NDK}") -message(CONFIGURE_LOG "Copying libc++_shared.so") - -# arm64-v8a -file(COPY - ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/arm64-v8a/libc++_shared.so - DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/arm64-v8a -) - -# armeabi-v7a -file(COPY - ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so - DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/armeabi-v7a -) - -# x86_64 -file(COPY - ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/x86_64/libc++_shared.so - DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/x86_64 -) \ No newline at end of file diff --git a/frontend/appflowy_flutter/android/app/src/main/Classes/binding.h b/frontend/appflowy_flutter/android/app/src/main/Classes/binding.h deleted file mode 100644 index 78992141ca..0000000000 --- a/frontend/appflowy_flutter/android/app/src/main/Classes/binding.h +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include -#include -#include - -int64_t init_sdk(int64_t port, char *data); - -void async_event(int64_t port, const uint8_t *input, uintptr_t len); - -const uint8_t *sync_event(const uint8_t *input, uintptr_t len); - -int32_t set_stream_port(int64_t port); - -int32_t set_log_stream_port(int64_t port); - -void link_me_please(void); - -void rust_log(int64_t level, const char *data); - -void set_env(const char *data); 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..db77bb4b7b 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..17987b79bb 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..09d4391482 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..d5f1c8d34e 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..4d6372eebd 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/android/app/src/profile/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/profile/AndroidManifest.xml index f880684a6a..7d5632662e 100644 --- a/frontend/appflowy_flutter/android/app/src/profile/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/profile/AndroidManifest.xml @@ -1,4 +1,5 @@ - + diff --git a/frontend/appflowy_flutter/android/build.gradle b/frontend/appflowy_flutter/android/build.gradle index aca8b4a201..09fbd6404c 100644 --- a/frontend/appflowy_flutter/android/build.gradle +++ b/frontend/appflowy_flutter/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.8.0' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -tasks.register("clean", Delete) { +task clean(type: Delete) { delete rootProject.buildDir } diff --git a/frontend/appflowy_flutter/android/gradle.properties b/frontend/appflowy_flutter/android/gradle.properties index 3aaa740c58..792b531d57 100644 --- a/frontend/appflowy_flutter/android/gradle.properties +++ b/frontend/appflowy_flutter/android/gradle.properties @@ -2,4 +2,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true org.gradle.caching=true -android.suppressUnsupportedCompileSdk=33 diff --git a/frontend/appflowy_flutter/android/gradle/wrapper/gradle-wrapper.properties b/frontend/appflowy_flutter/android/gradle/wrapper/gradle-wrapper.properties index fe1a99c2af..1acc777d74 100644 --- a/frontend/appflowy_flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/frontend/appflowy_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip -networkTimeout=10000 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists 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/google_fonts/Roboto_Mono/LICENSE.txt b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt deleted file mode 100644 index 75b52484ea..0000000000 --- a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf deleted file mode 100644 index 61e5303325..0000000000 Binary files a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf deleted file mode 100644 index 6df2b25360..0000000000 Binary files a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf and /dev/null 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/images/app_flowy_logo.jpg b/frontend/appflowy_flutter/assets/images/app_flowy_logo.jpg new file mode 100644 index 0000000000..bb27e0ddb1 Binary files /dev/null and b/frontend/appflowy_flutter/assets/images/app_flowy_logo.jpg differ diff --git a/frontend/appflowy_flutter/assets/images/appearance/dark.png b/frontend/appflowy_flutter/assets/images/appearance/dark.png deleted file mode 100644 index f40e6a884b..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/appearance/dark.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/appearance/light.png b/frontend/appflowy_flutter/assets/images/appearance/light.png deleted file mode 100644 index 49f32bf3aa..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/appearance/light.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/appearance/system.png b/frontend/appflowy_flutter/assets/images/appearance/system.png deleted file mode 100644 index 4097cae1ed..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/appearance/system.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_1.png b/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_1.png deleted file mode 100644 index fb72022287..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_1.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_2.png b/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_2.png deleted file mode 100644 index 9ecf02d253..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_2.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_3.png b/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_3.png deleted file mode 100644 index 97072b04f4..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_3.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_4.png b/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_4.png deleted file mode 100644 index 00d26a0500..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_4.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_5.png b/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_5.png deleted file mode 100644 index 3ecc9546c1..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_5.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_6.png b/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_6.png deleted file mode 100644 index 0abd2700e8..0000000000 Binary files a/frontend/appflowy_flutter/assets/images/built_in_cover_images/m_cover_image_6.png and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/images/common/archive.svg b/frontend/appflowy_flutter/assets/images/common/archive.svg new file mode 100644 index 0000000000..590dad7c38 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/common/archive.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/common/information.svg b/frontend/appflowy_flutter/assets/images/common/information.svg new file mode 100644 index 0000000000..3ff0998a03 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/common/information.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_flutter/assets/images/common/open_folder.svg b/frontend/appflowy_flutter/assets/images/common/open_folder.svg new file mode 100644 index 0000000000..cd81df9271 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/common/open_folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/common/recover.svg b/frontend/appflowy_flutter/assets/images/common/recover.svg new file mode 100644 index 0000000000..38d77b51de --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/common/recover.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/common/settings.svg b/frontend/appflowy_flutter/assets/images/common/settings.svg new file mode 100644 index 0000000000..92140a3c23 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/common/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/arrow_left.svg b/frontend/appflowy_flutter/assets/images/editor/Arrow/left.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/arrow_left.svg rename to frontend/appflowy_flutter/assets/images/editor/Arrow/left.svg diff --git a/frontend/resources/flowy_icons/16x/arrow_right.svg b/frontend/appflowy_flutter/assets/images/editor/Arrow/right.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/arrow_right.svg rename to frontend/appflowy_flutter/assets/images/editor/Arrow/right.svg diff --git a/frontend/resources/flowy_icons/16x/color_default.svg b/frontend/appflowy_flutter/assets/images/editor/Color/default.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/color_default.svg rename to frontend/appflowy_flutter/assets/images/editor/Color/default.svg diff --git a/frontend/resources/flowy_icons/16x/color_select.svg b/frontend/appflowy_flutter/assets/images/editor/Color/select.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/color_select.svg rename to frontend/appflowy_flutter/assets/images/editor/Color/select.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/Favorite/active.svg b/frontend/appflowy_flutter/assets/images/editor/Favorite/active.svg new file mode 100644 index 0000000000..8ad54bbbb5 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/Favorite/active.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/Favorite/default.svg b/frontend/appflowy_flutter/assets/images/editor/Favorite/default.svg new file mode 100644 index 0000000000..0ccfc1edff --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/Favorite/default.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/h1.svg b/frontend/appflowy_flutter/assets/images/editor/H1.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/h1.svg rename to frontend/appflowy_flutter/assets/images/editor/H1.svg diff --git a/frontend/resources/flowy_icons/16x/h2.svg b/frontend/appflowy_flutter/assets/images/editor/H2.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/h2.svg rename to frontend/appflowy_flutter/assets/images/editor/H2.svg diff --git a/frontend/resources/flowy_icons/16x/h3.svg b/frontend/appflowy_flutter/assets/images/editor/H3.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/h3.svg rename to frontend/appflowy_flutter/assets/images/editor/H3.svg diff --git a/frontend/resources/flowy_icons/16x/information.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Information.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/information.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Information.svg diff --git a/frontend/resources/flowy_icons/16x/lira.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Lira.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/lira.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Lira.svg diff --git a/frontend/resources/flowy_icons/16x/real.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Real.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/real.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Real.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/Icons 16/Relation.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Relation.svg new file mode 100644 index 0000000000..f82a41d226 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Relation.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/reload.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Reload.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/reload.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Reload.svg diff --git a/frontend/resources/flowy_icons/16x/ruble.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Ruble.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/ruble.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Ruble.svg diff --git a/frontend/resources/flowy_icons/16x/rupee.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Rupee.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/rupee.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Rupee.svg diff --git a/frontend/resources/flowy_icons/16x/rupiah.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Rupiah.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/rupiah.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Rupiah.svg diff --git a/frontend/resources/flowy_icons/16x/sort_high.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Sort/High.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/sort_high.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Sort/High.svg diff --git a/frontend/resources/flowy_icons/16x/sort_low.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Sort/Low.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/sort_low.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Sort/Low.svg diff --git a/frontend/resources/flowy_icons/16x/won.svg b/frontend/appflowy_flutter/assets/images/editor/Icons 16/Won.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/won.svg rename to frontend/appflowy_flutter/assets/images/editor/Icons 16/Won.svg diff --git a/frontend/resources/flowy_icons/16x/done.svg b/frontend/appflowy_flutter/assets/images/editor/Status/done.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/done.svg rename to frontend/appflowy_flutter/assets/images/editor/Status/done.svg diff --git a/frontend/resources/flowy_icons/16x/in_progress.svg b/frontend/appflowy_flutter/assets/images/editor/Status/inprogress.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/in_progress.svg rename to frontend/appflowy_flutter/assets/images/editor/Status/inprogress.svg diff --git a/frontend/resources/flowy_icons/16x/to_do.svg b/frontend/appflowy_flutter/assets/images/editor/Status/todo.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/to_do.svg rename to frontend/appflowy_flutter/assets/images/editor/Status/todo.svg diff --git a/frontend/resources/flowy_icons/16x/usd.svg b/frontend/appflowy_flutter/assets/images/editor/USD.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/usd.svg rename to frontend/appflowy_flutter/assets/images/editor/USD.svg diff --git a/frontend/resources/flowy_icons/16x/add.svg b/frontend/appflowy_flutter/assets/images/editor/add.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/add.svg rename to frontend/appflowy_flutter/assets/images/editor/add.svg diff --git a/frontend/resources/flowy_icons/16x/align_center.svg b/frontend/appflowy_flutter/assets/images/editor/align/center.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/align_center.svg rename to frontend/appflowy_flutter/assets/images/editor/align/center.svg diff --git a/frontend/resources/flowy_icons/16x/align_left.svg b/frontend/appflowy_flutter/assets/images/editor/align/left.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/align_left.svg rename to frontend/appflowy_flutter/assets/images/editor/align/left.svg diff --git a/frontend/resources/flowy_icons/16x/align_right.svg b/frontend/appflowy_flutter/assets/images/editor/align/right.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/align_right.svg rename to frontend/appflowy_flutter/assets/images/editor/align/right.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/attach.svg b/frontend/appflowy_flutter/assets/images/editor/attach.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/board.svg b/frontend/appflowy_flutter/assets/images/editor/board.svg new file mode 100644 index 0000000000..550d045178 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/board.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/bold.svg b/frontend/appflowy_flutter/assets/images/editor/bold.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/bold.svg rename to frontend/appflowy_flutter/assets/images/editor/bold.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/bullet_list.svg b/frontend/appflowy_flutter/assets/images/editor/bullet_list.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/bullet_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/calendar.svg b/frontend/appflowy_flutter/assets/images/editor/calendar.svg new file mode 100644 index 0000000000..c69687bc2a --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/calendar.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/check.svg b/frontend/appflowy_flutter/assets/images/editor/check.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/check.svg rename to frontend/appflowy_flutter/assets/images/editor/check.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/checkbox.svg b/frontend/appflowy_flutter/assets/images/editor/checkbox.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/checklist.svg b/frontend/appflowy_flutter/assets/images/editor/checklist.svg new file mode 100644 index 0000000000..3a88d236a1 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/checklist.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/clear.svg b/frontend/appflowy_flutter/assets/images/editor/clear.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/clear.svg rename to frontend/appflowy_flutter/assets/images/editor/clear.svg diff --git a/frontend/resources/flowy_icons/16x/close.svg b/frontend/appflowy_flutter/assets/images/editor/close.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/close.svg rename to frontend/appflowy_flutter/assets/images/editor/close.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/color_formatter.svg b/frontend/appflowy_flutter/assets/images/editor/color_formatter.svg new file mode 100644 index 0000000000..35d6d2ed67 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/color_formatter.svg @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/comment.svg b/frontend/appflowy_flutter/assets/images/editor/comment.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/comment.svg rename to frontend/appflowy_flutter/assets/images/editor/comment.svg diff --git a/frontend/resources/flowy_icons/16x/comments.svg b/frontend/appflowy_flutter/assets/images/editor/comments.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/comments.svg rename to frontend/appflowy_flutter/assets/images/editor/comments.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/copy.svg b/frontend/appflowy_flutter/assets/images/editor/copy.svg new file mode 100644 index 0000000000..f11048fd2f --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/dashboard.svg b/frontend/appflowy_flutter/assets/images/editor/dashboard.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/dashboard.svg rename to frontend/appflowy_flutter/assets/images/editor/dashboard.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/date.svg b/frontend/appflowy_flutter/assets/images/editor/date.svg new file mode 100644 index 0000000000..78243f1e75 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/date.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/delete.svg b/frontend/appflowy_flutter/assets/images/editor/delete.svg new file mode 100644 index 0000000000..cdf24226b4 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/details.svg b/frontend/appflowy_flutter/assets/images/editor/details.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/details.svg rename to frontend/appflowy_flutter/assets/images/editor/details.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/documents.svg b/frontend/appflowy_flutter/assets/images/editor/documents.svg new file mode 100644 index 0000000000..e232eba85d --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/documents.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/drag_element.svg b/frontend/appflowy_flutter/assets/images/editor/drag_element.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/drag_element.svg rename to frontend/appflowy_flutter/assets/images/editor/drag_element.svg diff --git a/frontend/resources/flowy_icons/16x/drop_menu_hide.svg b/frontend/appflowy_flutter/assets/images/editor/drop_menu/hide.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/drop_menu_hide.svg rename to frontend/appflowy_flutter/assets/images/editor/drop_menu/hide.svg diff --git a/frontend/resources/flowy_icons/16x/drop_menu_show.svg b/frontend/appflowy_flutter/assets/images/editor/drop_menu/show.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/drop_menu_show.svg rename to frontend/appflowy_flutter/assets/images/editor/drop_menu/show.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/duplicate.svg b/frontend/appflowy_flutter/assets/images/editor/duplicate.svg new file mode 100644 index 0000000000..1ae2067c24 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/duplicate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/edit.svg b/frontend/appflowy_flutter/assets/images/editor/edit.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/edit.svg rename to frontend/appflowy_flutter/assets/images/editor/edit.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/editor_check.svg b/frontend/appflowy_flutter/assets/images/editor/editor_check.svg new file mode 100644 index 0000000000..8446cced9f --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/editor_check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/editor_uncheck.svg b/frontend/appflowy_flutter/assets/images/editor/editor_uncheck.svg new file mode 100644 index 0000000000..6c487795c6 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/editor_uncheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/euro.svg b/frontend/appflowy_flutter/assets/images/editor/euro.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/euro.svg rename to frontend/appflowy_flutter/assets/images/editor/euro.svg diff --git a/frontend/resources/flowy_icons/16x/full_view.svg b/frontend/appflowy_flutter/assets/images/editor/full_view.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/full_view.svg rename to frontend/appflowy_flutter/assets/images/editor/full_view.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/grid.svg b/frontend/appflowy_flutter/assets/images/editor/grid.svg new file mode 100644 index 0000000000..8164b24e6a --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/group.svg b/frontend/appflowy_flutter/assets/images/editor/group.svg new file mode 100644 index 0000000000..f0a6dff4f9 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/group.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/hide.svg b/frontend/appflowy_flutter/assets/images/editor/hide.svg new file mode 100644 index 0000000000..45e81d8748 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/hide.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/highlight.svg b/frontend/appflowy_flutter/assets/images/editor/highlight.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/highlight.svg rename to frontend/appflowy_flutter/assets/images/editor/highlight.svg diff --git a/frontend/resources/flowy_icons/16x/image.svg b/frontend/appflowy_flutter/assets/images/editor/image.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/image.svg rename to frontend/appflowy_flutter/assets/images/editor/image.svg diff --git a/frontend/resources/flowy_icons/16x/import.svg b/frontend/appflowy_flutter/assets/images/editor/import.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/import.svg rename to frontend/appflowy_flutter/assets/images/editor/import.svg diff --git a/frontend/resources/flowy_icons/16x/embed_link.svg b/frontend/appflowy_flutter/assets/images/editor/inline_block.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/embed_link.svg rename to frontend/appflowy_flutter/assets/images/editor/inline_block.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/insert_emoticon.svg b/frontend/appflowy_flutter/assets/images/editor/insert_emoticon.svg new file mode 100644 index 0000000000..8bb960e52d --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/insert_emoticon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/insert_emoticon_2.svg b/frontend/appflowy_flutter/assets/images/editor/insert_emoticon_2.svg new file mode 100644 index 0000000000..66bbf7a626 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/insert_emoticon_2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/italic.svg b/frontend/appflowy_flutter/assets/images/editor/italic.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/italic.svg rename to frontend/appflowy_flutter/assets/images/editor/italic.svg diff --git a/frontend/resources/flowy_icons/16x/left.svg b/frontend/appflowy_flutter/assets/images/editor/left.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/left.svg rename to frontend/appflowy_flutter/assets/images/editor/left.svg diff --git a/frontend/resources/flowy_icons/16x/level.svg b/frontend/appflowy_flutter/assets/images/editor/level.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/level.svg rename to frontend/appflowy_flutter/assets/images/editor/level.svg diff --git a/frontend/resources/flowy_icons/16x/share.svg b/frontend/appflowy_flutter/assets/images/editor/link.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/share.svg rename to frontend/appflowy_flutter/assets/images/editor/link.svg diff --git a/frontend/resources/flowy_icons/16x/list_dropdown.svg b/frontend/appflowy_flutter/assets/images/editor/list_dropdown.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/list_dropdown.svg rename to frontend/appflowy_flutter/assets/images/editor/list_dropdown.svg diff --git a/frontend/resources/flowy_icons/16x/logout.svg b/frontend/appflowy_flutter/assets/images/editor/logout.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/logout.svg rename to frontend/appflowy_flutter/assets/images/editor/logout.svg diff --git a/frontend/resources/flowy_icons/16x/messages.svg b/frontend/appflowy_flutter/assets/images/editor/messages.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/messages.svg rename to frontend/appflowy_flutter/assets/images/editor/messages.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/more.svg b/frontend/appflowy_flutter/assets/images/editor/more.svg new file mode 100644 index 0000000000..b191e64a10 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/more.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/numbers.svg b/frontend/appflowy_flutter/assets/images/editor/numbers.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/numbers.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/option.svg b/frontend/appflowy_flutter/assets/images/editor/option.svg new file mode 100644 index 0000000000..627c959f9f --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/option.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/page.svg b/frontend/appflowy_flutter/assets/images/editor/page.svg new file mode 100644 index 0000000000..f0fb8ce9ce --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/page.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/percent.svg b/frontend/appflowy_flutter/assets/images/editor/percent.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/percent.svg rename to frontend/appflowy_flutter/assets/images/editor/percent.svg diff --git a/frontend/resources/flowy_icons/16x/mention.svg b/frontend/appflowy_flutter/assets/images/editor/persoin_1.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/mention.svg rename to frontend/appflowy_flutter/assets/images/editor/persoin_1.svg diff --git a/frontend/resources/flowy_icons/16x/person.svg b/frontend/appflowy_flutter/assets/images/editor/person.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/person.svg rename to frontend/appflowy_flutter/assets/images/editor/person.svg diff --git a/frontend/resources/flowy_icons/16x/pound.svg b/frontend/appflowy_flutter/assets/images/editor/pound.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/pound.svg rename to frontend/appflowy_flutter/assets/images/editor/pound.svg diff --git a/frontend/resources/flowy_icons/16x/quote.svg b/frontend/appflowy_flutter/assets/images/editor/quote.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/quote.svg rename to frontend/appflowy_flutter/assets/images/editor/quote.svg diff --git a/frontend/resources/flowy_icons/16x/report.svg b/frontend/appflowy_flutter/assets/images/editor/report.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/report.svg rename to frontend/appflowy_flutter/assets/images/editor/report.svg diff --git a/frontend/resources/flowy_icons/16x/resize.svg b/frontend/appflowy_flutter/assets/images/editor/resize.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/resize.svg rename to frontend/appflowy_flutter/assets/images/editor/resize.svg diff --git a/frontend/resources/flowy_icons/16x/restore.svg b/frontend/appflowy_flutter/assets/images/editor/restore.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/restore.svg rename to frontend/appflowy_flutter/assets/images/editor/restore.svg diff --git a/frontend/resources/flowy_icons/16x/right.svg b/frontend/appflowy_flutter/assets/images/editor/right.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/right.svg rename to frontend/appflowy_flutter/assets/images/editor/right.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/search.svg b/frontend/appflowy_flutter/assets/images/editor/search.svg new file mode 100644 index 0000000000..1efb2d475c --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/send.svg b/frontend/appflowy_flutter/assets/images/editor/send.svg new file mode 100644 index 0000000000..a5f933a8ca --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/settings.svg b/frontend/appflowy_flutter/assets/images/editor/settings.svg new file mode 100644 index 0000000000..f9896aad52 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/share.svg b/frontend/appflowy_flutter/assets/images/editor/share.svg new file mode 100644 index 0000000000..5fbcc8d787 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/share.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/show_menu.svg b/frontend/appflowy_flutter/assets/images/editor/show_menu.svg new file mode 100644 index 0000000000..8baf55bffd --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/show_menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/16x/slash.svg b/frontend/appflowy_flutter/assets/images/editor/slash.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/slash.svg rename to frontend/appflowy_flutter/assets/images/editor/slash.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/status.svg b/frontend/appflowy_flutter/assets/images/editor/status.svg new file mode 100644 index 0000000000..8ccbc9a2e3 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/status.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/strikethrough.svg b/frontend/appflowy_flutter/assets/images/editor/strikethrough.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/strikethrough.svg rename to frontend/appflowy_flutter/assets/images/editor/strikethrough.svg diff --git a/frontend/resources/flowy_icons/16x/tag.svg b/frontend/appflowy_flutter/assets/images/editor/tag.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/tag.svg rename to frontend/appflowy_flutter/assets/images/editor/tag.svg diff --git a/frontend/resources/flowy_icons/16x/tag_block.svg b/frontend/appflowy_flutter/assets/images/editor/tag_block.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/tag_block.svg rename to frontend/appflowy_flutter/assets/images/editor/tag_block.svg diff --git a/frontend/resources/flowy_icons/16x/template.svg b/frontend/appflowy_flutter/assets/images/editor/template.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/template.svg rename to frontend/appflowy_flutter/assets/images/editor/template.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/text.svg b/frontend/appflowy_flutter/assets/images/editor/text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/time.svg b/frontend/appflowy_flutter/assets/images/editor/time.svg new file mode 100644 index 0000000000..634af3e361 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/time.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/timer_finish.svg b/frontend/appflowy_flutter/assets/images/editor/timer_finish.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/timer_finish.svg rename to frontend/appflowy_flutter/assets/images/editor/timer_finish.svg diff --git a/frontend/resources/flowy_icons/16x/timer_start.svg b/frontend/appflowy_flutter/assets/images/editor/timer_start.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/timer_start.svg rename to frontend/appflowy_flutter/assets/images/editor/timer_start.svg diff --git a/frontend/resources/flowy_icons/16x/toggle_list.svg b/frontend/appflowy_flutter/assets/images/editor/toggle_list.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/toggle_list.svg rename to frontend/appflowy_flutter/assets/images/editor/toggle_list.svg diff --git a/frontend/resources/flowy_icons/16x/underline.svg b/frontend/appflowy_flutter/assets/images/editor/underline.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/underline.svg rename to frontend/appflowy_flutter/assets/images/editor/underline.svg diff --git a/frontend/appflowy_flutter/assets/images/file_icon.jpg b/frontend/appflowy_flutter/assets/images/file_icon.jpg new file mode 100644 index 0000000000..88865fa004 Binary files /dev/null and b/frontend/appflowy_flutter/assets/images/file_icon.jpg differ diff --git a/frontend/appflowy_flutter/assets/images/file_icon.svg b/frontend/appflowy_flutter/assets/images/file_icon.svg new file mode 100644 index 0000000000..f0fb8ce9ce --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/file_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/folder.svg b/frontend/appflowy_flutter/assets/images/folder.svg new file mode 100644 index 0000000000..9c4d0dddb0 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/folder.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/images/grid/checkmark.svg b/frontend/appflowy_flutter/assets/images/grid/checkmark.svg new file mode 100644 index 0000000000..f9c848f713 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/clock_alarm.svg b/frontend/appflowy_flutter/assets/images/grid/clock.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/clock_alarm.svg rename to frontend/appflowy_flutter/assets/images/grid/clock.svg diff --git a/frontend/resources/flowy_icons/16x/delete.svg b/frontend/appflowy_flutter/assets/images/grid/delete.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/delete.svg rename to frontend/appflowy_flutter/assets/images/grid/delete.svg diff --git a/frontend/appflowy_flutter/assets/images/grid/details.svg b/frontend/appflowy_flutter/assets/images/grid/details.svg new file mode 100644 index 0000000000..e4c9f58f27 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/details.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/duplicate.svg b/frontend/appflowy_flutter/assets/images/grid/duplicate.svg new file mode 100644 index 0000000000..f11048fd2f --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/duplicate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/expander.svg b/frontend/appflowy_flutter/assets/images/grid/expander.svg new file mode 100644 index 0000000000..179bdb1a9e --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/expander.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/checkbox.svg b/frontend/appflowy_flutter/assets/images/grid/field/checkbox.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/checklist.svg b/frontend/appflowy_flutter/assets/images/grid/field/checklist.svg new file mode 100644 index 0000000000..3a88d236a1 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/checklist.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/date.svg b/frontend/appflowy_flutter/assets/images/grid/field/date.svg new file mode 100644 index 0000000000..78243f1e75 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/date.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/euro.svg b/frontend/appflowy_flutter/assets/images/grid/field/euro.svg new file mode 100644 index 0000000000..95f511f687 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/euro.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/multi_select.svg b/frontend/appflowy_flutter/assets/images/grid/field/multi_select.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/multi_select.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/number.svg b/frontend/appflowy_flutter/assets/images/grid/field/number.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/number.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/numbers.svg b/frontend/appflowy_flutter/assets/images/grid/field/numbers.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/numbers.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/single_select.svg b/frontend/appflowy_flutter/assets/images/grid/field/single_select.svg new file mode 100644 index 0000000000..8ccbc9a2e3 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/single_select.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/text.svg b/frontend/appflowy_flutter/assets/images/grid/field/text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/url.svg b/frontend/appflowy_flutter/assets/images/grid/field/url.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/url.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/us_dollar.svg b/frontend/appflowy_flutter/assets/images/grid/field/us_dollar.svg new file mode 100644 index 0000000000..a8485cd6a1 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/us_dollar.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/field/yen.svg b/frontend/appflowy_flutter/assets/images/grid/field/yen.svg new file mode 100644 index 0000000000..8e9bf47c99 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/field/yen.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/hide.svg b/frontend/appflowy_flutter/assets/images/grid/hide.svg new file mode 100644 index 0000000000..dfb6dbb90c --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/hide.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/left.svg b/frontend/appflowy_flutter/assets/images/grid/left.svg new file mode 100644 index 0000000000..0f771a3858 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/more.svg b/frontend/appflowy_flutter/assets/images/grid/more.svg new file mode 100644 index 0000000000..b191e64a10 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/more.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/grid/right.svg b/frontend/appflowy_flutter/assets/images/grid/right.svg new file mode 100644 index 0000000000..7d738f4e69 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/sort_ascending.svg b/frontend/appflowy_flutter/assets/images/grid/setting/ascending.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/sort_ascending.svg rename to frontend/appflowy_flutter/assets/images/grid/setting/ascending.svg diff --git a/frontend/appflowy_flutter/assets/images/grid/setting/calendar_layout.svg b/frontend/appflowy_flutter/assets/images/grid/setting/calendar_layout.svg new file mode 100644 index 0000000000..32423640d8 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/setting/calendar_layout.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/images/grid/setting/database_layout.svg b/frontend/appflowy_flutter/assets/images/grid/setting/database_layout.svg new file mode 100644 index 0000000000..e1bf39190a --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/setting/database_layout.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/sort_descending.svg b/frontend/appflowy_flutter/assets/images/grid/setting/descending.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/sort_descending.svg rename to frontend/appflowy_flutter/assets/images/grid/setting/descending.svg diff --git a/frontend/resources/flowy_icons/16x/filter.svg b/frontend/appflowy_flutter/assets/images/grid/setting/filter.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/filter.svg rename to frontend/appflowy_flutter/assets/images/grid/setting/filter.svg diff --git a/frontend/appflowy_flutter/assets/images/grid/setting/group.svg b/frontend/appflowy_flutter/assets/images/grid/setting/group.svg new file mode 100644 index 0000000000..f0a6dff4f9 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/setting/group.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/properties.svg b/frontend/appflowy_flutter/assets/images/grid/setting/properties.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/properties.svg rename to frontend/appflowy_flutter/assets/images/grid/setting/properties.svg diff --git a/frontend/resources/flowy_icons/24x/settings.svg b/frontend/appflowy_flutter/assets/images/grid/setting/setting.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/settings.svg rename to frontend/appflowy_flutter/assets/images/grid/setting/setting.svg diff --git a/frontend/appflowy_flutter/assets/images/grid/setting/sort.svg b/frontend/appflowy_flutter/assets/images/grid/setting/sort.svg new file mode 100644 index 0000000000..06e17d62a9 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/grid/setting/sort.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/32x/favorite_active.svg b/frontend/appflowy_flutter/assets/images/home/Favorite/active.svg similarity index 100% rename from frontend/resources/flowy_icons/32x/favorite_active.svg rename to frontend/appflowy_flutter/assets/images/home/Favorite/active.svg diff --git a/frontend/resources/flowy_icons/32x/favorite_inactive.svg b/frontend/appflowy_flutter/assets/images/home/Favorite/inactive.svg similarity index 100% rename from frontend/resources/flowy_icons/32x/favorite_inactive.svg rename to frontend/appflowy_flutter/assets/images/home/Favorite/inactive.svg diff --git a/frontend/resources/flowy_icons/24x/sort_high.svg b/frontend/appflowy_flutter/assets/images/home/Sort/high.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/sort_high.svg rename to frontend/appflowy_flutter/assets/images/home/Sort/high.svg diff --git a/frontend/resources/flowy_icons/24x/sort_low.svg b/frontend/appflowy_flutter/assets/images/home/Sort/low.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/sort_low.svg rename to frontend/appflowy_flutter/assets/images/home/Sort/low.svg diff --git a/frontend/resources/flowy_icons/24x/add.svg b/frontend/appflowy_flutter/assets/images/home/add.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/add.svg rename to frontend/appflowy_flutter/assets/images/home/add.svg diff --git a/frontend/resources/flowy_icons/24x/arrow_left.svg b/frontend/appflowy_flutter/assets/images/home/arrow_left.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/arrow_left.svg rename to frontend/appflowy_flutter/assets/images/home/arrow_left.svg diff --git a/frontend/resources/flowy_icons/24x/arrow_right.svg b/frontend/appflowy_flutter/assets/images/home/arrow_right.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/arrow_right.svg rename to frontend/appflowy_flutter/assets/images/home/arrow_right.svg diff --git a/frontend/resources/flowy_icons/32x/close.svg b/frontend/appflowy_flutter/assets/images/home/close.svg similarity index 100% rename from frontend/resources/flowy_icons/32x/close.svg rename to frontend/appflowy_flutter/assets/images/home/close.svg diff --git a/frontend/resources/flowy_icons/24x/dashboard.svg b/frontend/appflowy_flutter/assets/images/home/dashboard.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/dashboard.svg rename to frontend/appflowy_flutter/assets/images/home/dashboard.svg diff --git a/frontend/resources/flowy_icons/24x/details.svg b/frontend/appflowy_flutter/assets/images/home/details.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/details.svg rename to frontend/appflowy_flutter/assets/images/home/details.svg diff --git a/frontend/resources/flowy_icons/24x/drop_menu_hide.svg b/frontend/appflowy_flutter/assets/images/home/drop_down_hide.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/drop_menu_hide.svg rename to frontend/appflowy_flutter/assets/images/home/drop_down_hide.svg diff --git a/frontend/resources/flowy_icons/24x/drop_menu_show.svg b/frontend/appflowy_flutter/assets/images/home/drop_down_show.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/drop_menu_show.svg rename to frontend/appflowy_flutter/assets/images/home/drop_down_show.svg diff --git a/frontend/resources/flowy_icons/24x/ethernet.svg b/frontend/appflowy_flutter/assets/images/home/eathernet.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/ethernet.svg rename to frontend/appflowy_flutter/assets/images/home/eathernet.svg diff --git a/frontend/resources/flowy_icons/24x/favorite.svg b/frontend/appflowy_flutter/assets/images/home/favorite.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/favorite.svg rename to frontend/appflowy_flutter/assets/images/home/favorite.svg diff --git a/frontend/resources/flowy_icons/24x/hide.svg b/frontend/appflowy_flutter/assets/images/home/hide.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/hide.svg rename to frontend/appflowy_flutter/assets/images/home/hide.svg diff --git a/frontend/resources/flowy_icons/24x/hide_menu.svg b/frontend/appflowy_flutter/assets/images/home/hide_menu.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/hide_menu.svg rename to frontend/appflowy_flutter/assets/images/home/hide_menu.svg diff --git a/frontend/resources/flowy_icons/32x/image.svg b/frontend/appflowy_flutter/assets/images/home/image.svg similarity index 100% rename from frontend/resources/flowy_icons/32x/image.svg rename to frontend/appflowy_flutter/assets/images/home/image.svg diff --git a/frontend/resources/flowy_icons/24x/level.svg b/frontend/appflowy_flutter/assets/images/home/level.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/level.svg rename to frontend/appflowy_flutter/assets/images/home/level.svg diff --git a/frontend/resources/flowy_icons/24x/messages.svg b/frontend/appflowy_flutter/assets/images/home/messages.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/messages.svg rename to frontend/appflowy_flutter/assets/images/home/messages.svg diff --git a/frontend/appflowy_flutter/assets/images/home/new_app.svg b/frontend/appflowy_flutter/assets/images/home/new_app.svg new file mode 100644 index 0000000000..c74ac3b349 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/home/new_app.svg @@ -0,0 +1,17 @@ + + + + diff --git a/frontend/resources/flowy_icons/32x/page.svg b/frontend/appflowy_flutter/assets/images/home/page.svg similarity index 100% rename from frontend/resources/flowy_icons/32x/page.svg rename to frontend/appflowy_flutter/assets/images/home/page.svg diff --git a/frontend/resources/flowy_icons/24x/person.svg b/frontend/appflowy_flutter/assets/images/home/person.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/person.svg rename to frontend/appflowy_flutter/assets/images/home/person.svg diff --git a/frontend/resources/flowy_icons/24x/search.svg b/frontend/appflowy_flutter/assets/images/home/search.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/search.svg rename to frontend/appflowy_flutter/assets/images/home/search.svg diff --git a/frontend/appflowy_flutter/assets/images/home/settings.svg b/frontend/appflowy_flutter/assets/images/home/settings.svg new file mode 100644 index 0000000000..3d632703ab --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/home/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/share.svg b/frontend/appflowy_flutter/assets/images/home/share.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/share.svg rename to frontend/appflowy_flutter/assets/images/home/share.svg diff --git a/frontend/appflowy_flutter/assets/images/home/show.svg b/frontend/appflowy_flutter/assets/images/home/show.svg new file mode 100644 index 0000000000..3550115093 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/home/show.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/trash.svg b/frontend/appflowy_flutter/assets/images/home/trash.svg similarity index 100% rename from frontend/resources/flowy_icons/24x/trash.svg rename to frontend/appflowy_flutter/assets/images/home/trash.svg diff --git a/frontend/appflowy_flutter/assets/images/login/github-light.svg b/frontend/appflowy_flutter/assets/images/login/github-light.svg deleted file mode 100644 index 5128fd0cda..0000000000 --- a/frontend/appflowy_flutter/assets/images/login/github-light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_flutter/assets/template/readme.json b/frontend/appflowy_flutter/assets/template/readme.json index 9fab2df113..c5aea3838e 100644 --- a/frontend/appflowy_flutter/assets/template/readme.json +++ b/frontend/appflowy_flutter/assets/template/readme.json @@ -73,9 +73,9 @@ "attributes": { "subtype": "checkbox", "checkbox": true }, "delta": [ { "insert": "Click " }, - { "insert": "+ New Page", "attributes": { "code": true } }, + { "insert": "+ New Page ", "attributes": { "code": true } }, { - "insert": " button at the bottom of your sidebar to add a new page." + "insert": "button at the bottom of your sidebar to add a new page." } ] }, diff --git a/frontend/appflowy_flutter/assets/test/images/sample.gif b/frontend/appflowy_flutter/assets/test/images/sample.gif deleted file mode 100644 index b3b85f4a40..0000000000 Binary files a/frontend/appflowy_flutter/assets/test/images/sample.gif and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/test/images/sample.jpeg b/frontend/appflowy_flutter/assets/test/images/sample.jpeg deleted file mode 100644 index d2f50a4148..0000000000 Binary files a/frontend/appflowy_flutter/assets/test/images/sample.jpeg and /dev/null differ diff --git a/frontend/appflowy_flutter/assets/test/images/sample.png b/frontend/appflowy_flutter/assets/test/images/sample.png deleted file mode 100644 index 84f633437f..0000000000 Binary files a/frontend/appflowy_flutter/assets/test/images/sample.png and /dev/null differ 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/test/workspaces/markdowns/markdown_with_table.md b/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md deleted file mode 100644 index 5998220774..0000000000 --- a/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md +++ /dev/null @@ -1,11 +0,0 @@ -# AppFlowy Test Markdown import with table - -# Table - -| S.No. | Column 2 | -| --- | --- | -| 1. | row 1 | -| 2. | row 2 | -| 3. | row 3 | -| 4. | row 4 | -| 5. | row 5 | \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/ar-SA.json b/frontend/appflowy_flutter/assets/translations/ar-SA.json new file mode 100644 index 0000000000..a1f6446428 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/ar-SA.json @@ -0,0 +1,422 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "أنا", + "welcomeText": "مرحبًا بك في @: appName", + "githubStarText": "نجمة على GitHub", + "subscribeNewsletterText": "اشترك في النشرة الإخبارية", + "letsGoButtonText": "بداية سريعة", + "title": "عنوان", + "signUp": { + "buttonText": "اشتراك", + "title": "قم بالتسجيل في @: appName", + "getStartedText": "البدء", + "emptyPasswordError": "لا يمكن أن تكون كلمة المرور فارغة", + "repeatPasswordEmptyError": "إعادة كلمة المرور لا يمكن أن تكون فارغة", + "unmatchedPasswordError": "تكرار كلمة المرور ليس هو نفسه كلمة المرور", + "alreadyHaveAnAccount": "هل لديك حساب؟", + "emailHint": "بريد إلكتروني", + "passwordHint": "كلمة المرور", + "repeatPasswordHint": "اعد كلمة السر" + }, + "signIn": { + "loginTitle": "تسجيل الدخول إلى @: appName", + "loginButtonText": "تسجيل الدخول", + "buttonText": "تسجيل الدخول", + "forgotPassword": "هل نسيت كلمة السر؟", + "emailHint": "بريد إلكتروني", + "passwordHint": "كلمة المرور", + "dontHaveAnAccount": "ليس لديك حساب؟", + "repeatPasswordEmptyError": "إعادة كلمة المرور لا يمكن أن تكون فارغة", + "unmatchedPasswordError": "تكرار كلمة المرور ليس هو نفسه كلمة المرور" + }, + "workspace": { + "create": "قم بإنشاء مساحة عمل", + "hint": "مساحة العمل", + "notFoundError": "مساحة العمل غير موجودة" + }, + "shareAction": { + "buttonText": "مشاركه", + "workInProgress": "قريباً", + "markdown": "Markdown", + "copyLink": "نسخ الرابط" + }, + "moreAction": { + "small": "صغير", + "medium": "متوسط", + "large": "كبير", + "fontSize": "حجم الخط", + "import": "استيراد" + }, + "disclosureAction": { + "rename": "إعادة تسمية", + "delete": "يمسح", + "duplicate": "كرر" + }, + "blankPageTitle": "صفحة فارغة", + "newPageText": "صفحة جديدة", + "trash": { + "text": "المهملات", + "restoreAll": "استعادة الكل", + "deleteAll": "حذف الكل", + "pageHeader": { + "fileName": "اسم الملف", + "lastModified": "آخر تعديل", + "created": "تم انشاؤها" + } + }, + "deletePagePrompt": { + "text": "هذه الصفحة في المهملات", + "restore": "استعادة الصفحة", + "deletePermanent": "الحذف بشكل نهائي" + }, + "dialogCreatePageNameHint": "اسم الصفحة", + "questionBubble": { + "shortcuts": "الاختصارات", + "whatsNew": "ما هو الجديد؟", + "help": "المساعدة والدعم", + "markdown": "Markdown", + "debug": { + "name": "معلومات التصحيح", + "success": "تم نسخ معلومات التصحيح إلى الحافظة!", + "fail": "تعذر نسخ معلومات التصحيح إلى الحافظة" + } + }, + "menuAppHeader": { + "addPageTooltip": "أضف صفحة في الداخل بسرعة", + "defaultNewPageName": "بدون عنوان", + "renameDialog": "إعادة تسمية" + }, + "toolbar": { + "undo": "الغاء التحميل", + "redo": "إعادة", + "bold": "عريض", + "italic": "مائل", + "underline": "تسطير", + "strike": "يتوسطه خط", + "numList": "قائمة مرقمة", + "bulletList": "قائمة نقطية", + "checkList": "قائمة تدقيق", + "inlineCode": "رمز مضمّن", + "quote": "كتلة اقتباس", + "header": "رأس", + "highlight": "تسليط الضوء", + "color": "لون" + }, + "tooltip": { + "lightMode": "قم بالتبديل إلى وضع الإضاءة", + "darkMode": "قم بالتبديل إلى الوضع الداكن", + "openAsPage": "فتح كصفحة", + "addNewRow": "أضف صفًا جديدًا", + "openMenu": "انقر لفتح القائمة", + "viewDataBase": "عرض قاعدة البيانات", + "referencePage": "تمت الإشارة إلى هذا {name}" + }, + "sideBar": { + "closeSidebar": "إغلاق الشريط الجانبي", + "openSidebar": "فتح الشريط الجانبي" + }, + "notifications": { + "export": { + "markdown": "تم تصدير ملاحظة إلى Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "جهات الاتصال", + "whatsHappening": "ماذا يحدث هذا الاسبوع؟", + "addContact": "إضافة جهة اتصال", + "editContact": "تحرير جهة الاتصال" + }, + "button": { + "OK": "نعم", + "Done": "منتهي", + "Cancel": "إلغاء", + "signIn": "تسجيل الدخول", + "signOut": "خروج", + "complete": "مكتمل", + "save": "حفظ", + "generate": "يولد", + "esc": "خروج", + "keep": "ابقاء", + "tryAgain": "حاول ثانية", + "discard": "تجاهل", + "replace": "يستبدل", + "insertBelow": "إدراج أدناه" + }, + "label": { + "welcome": "مرحباً!", + "firstName": "الاسم الأول", + "middleName": "الاسم الأوسط", + "lastName": "اسم العائلة", + "stepX": "الخطوة {X}" + }, + "oAuth": { + "err": { + "failedTitle": "غير قادر على الاتصال بحسابك.", + "failedMsg": "يرجى التأكد من إكمال عملية تسجيل الدخول في متصفحك." + }, + "google": { + "title": "تسجيل الدخول إلى GOOGLE", + "instruction1": "لاستيراد جهات اتصال Google الخاصة بك ، ستحتاج إلى ترخيص هذا التطبيق باستخدام متصفح الويب الخاص بك.", + "instruction2": "انسخ هذا الرمز إلى الحافظة الخاصة بك عن طريق النقر فوق الرمز أو تحديد النص:", + "instruction3": "انتقل إلى الرابط التالي في متصفح الويب الخاص بك ، وأدخل الرمز أعلاه:", + "instruction4": "اضغط على الزر أدناه عند الانتهاء من التسجيل:" + } + }, + "settings": { + "title": "إعدادات", + "menu": { + "appearance": "مظهر", + "language": "لغة", + "user": "مستخدم", + "files": "الملفات", + "open": "أفتح الإعدادات" + }, + "appearance": { + "themeMode": { + "label": "وضع السمة", + "light": "وضع الضوء", + "dark": "الوضع الداكن", + "system": "التكيف مع النظام" + }, + "theme": "سمة" + }, + "files": { + "defaultLocation": "أين يتم تخزين بياناتك الآن", + "doubleTapToCopy": "انقر نقرًا مزدوجًا لنسخ المسار", + "restoreLocation": "استعادة المسار الافتراضي AppFlowy", + "customizeLocation": "افتح مجلدًا آخر", + "restartApp": "يرجى إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول.", + "exportDatabase": "تصدير قاعدة البيانات", + "selectFiles": "حدد الملفات التي تريد تصديرها", + "createNewFolder": "انشاء مجلد جديد", + "createNewFolderDesc": "أخبرنا بالمكان الذي تريد تخزين بياناتك فيه", + "open": "يفتح", + "openFolder": "افتح مجلدًا موجودًا", + "openFolderDesc": "اقرأها واكتبها في مجلد AppFlowy الموجود لديك", + "folderHintText": "إسم الملف", + "location": "إنشاء مجلد جديد", + "locationDesc": "اختر اسمًا لمجلد بيانات AppFlowy", + "browser": "تصفح", + "create": "يخلق", + "folderPath": "مسار لتخزين المجلد الخاص بك", + "locationCannotBeEmpty": "لا يمكن أن يكون المسار فارغًا", + "pathCopiedSnackbar": "تم نسخ مسار تخزين الملفات إلى الحافظة!" + }, + "user": { + "name": "اسم", + "icon": "أيقونة", + "selectAnIcon": "حدد أيقونة", + "pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك" + } + }, + "grid": { + "settings": { + "filter": "منقي", + "sort": "نوع", + "sortBy": "ترتيب حسب", + "Properties": "ملكيات", + "group": "مجموعة", + "addFilter": "أضف عامل تصفية", + "deleteFilter": "حذف عامل التصفية", + "filterBy": "مصنف بواسطة...", + "typeAValue": "اكتب قيمة ...", + "layout": "تَخطِيط" + }, + "textFilter": { + "contains": "يتضمن", + "doesNotContain": "لا يحتوي", + "endsWith": "ينتهي بـ", + "startWith": "ابدا ب", + "is": "يكون", + "isNot": "ليس", + "isEmpty": "فارغ", + "isNotEmpty": "ليس فارغا", + "choicechipPrefix": { + "isNot": "لا", + "startWith": "ابدا ب", + "endWith": "ينتهي بـ", + "isEmpty": "فارغ", + "isNotEmpty": "ليس فارغا" + } + }, + "checkboxFilter": { + "isChecked": "التحقق", + "isUnchecked": "لم يتم التحقق منه", + "choicechipPrefix": { + "is": "يكون" + } + }, + "checklistFilter": { + "isComplete": "كاملة", + "isIncomplted": "غير مكتمل" + }, + "singleSelectOptionFilter": { + "is": "يكون", + "isNot": "ليس", + "isEmpty": "فارغ", + "isNotEmpty": "ليس فارغا" + }, + "multiSelectOptionFilter": { + "contains": "يتضمن", + "doesNotContain": "لا يحتوي", + "isEmpty": "فارغ", + "isNotEmpty": "ليس فارغا" + }, + "field": { + "hide": "يخفي", + "insertLeft": "أدخل اليسار", + "insertRight": "أدخل اليمين", + "duplicate": "ينسخ", + "delete": "يمسح", + "textFieldName": "نص", + "checkboxFieldName": "خانة اختيار", + "dateFieldName": "تاريخ", + "numberFieldName": "أعداد", + "singleSelectFieldName": "يختار", + "multiSelectFieldName": "تحديد متعدد", + "urlFieldName": "URL", + "checklistFieldName": "قائمة تدقيق", + "numberFormat": "تنسيق الأرقام", + "dateFormat": "صيغة التاريخ", + "includeTime": "أضف الوقت", + "dateFormatFriendly": "شهر يوم سنه", + "dateFormatISO": "سنة شهر يوم", + "dateFormatLocal": "شهر يوم سنه", + "dateFormatUS": "سنة شهر يوم", + "dateFormatDayMonthYear": "يوم شهر سنة", + "timeFormat": "تنسيق الوقت", + "invalidTimeFormat": "تنسيق غير صالح", + "timeFormatTwelveHour": "12 ساعة", + "timeFormatTwentyFourHour": "24 ساعة", + "addSelectOption": "أضف خيارًا", + "optionTitle": "خيارات", + "addOption": "إضافة خيار", + "editProperty": "تحرير الملكية", + "newProperty": "خاصية جديدة", + "deleteFieldPromptMessage": "هل أنت متأكد؟ سيتم حذف هذه الخاصية" + }, + "sort": { + "ascending": "تصاعدي", + "descending": "تنازلي", + "deleteSort": "حذف الفرز", + "addSort": "أضف نوعًا" + }, + "row": { + "duplicate": "مكرره", + "delete": "يمسح", + "textPlaceholder": "فارغ", + "copyProperty": "نسخ الممتلكات إلى الحافظة", + "count": "عدد", + "newRow": "صف جديد" + }, + "selectOption": { + "create": "يخلق", + "purpleColor": "أرجواني", + "pinkColor": "لون القرنفل", + "lightPinkColor": "وردي فاتح", + "orangeColor": "البرتقالي", + "yellowColor": "أصفر", + "limeColor": "جير", + "greenColor": "أخضر", + "aquaColor": "أكوا", + "blueColor": "أزرق", + "deleteTag": "حذف العلامة", + "colorPanelTitle": "الألوان", + "panelTitle": "حدد خيارًا أو أنشئ خيارًا", + "searchOption": "ابحث عن خيار" + }, + "checklist": { + "panelTitle": "أضف عنصرًا" + }, + "menuName": "شبكة", + "referencedGridPrefix": "نظرا ل" + }, + "document": { + "menuName": "وثيقة", + "date": { + "timeHintTextInTwelveHour": "01:00 مساءً", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "حدد لوحة للارتباط بها", + "createANewBoard": "قم بإنشاء لوحة جديدة" + }, + "grid": { + "selectAGridToLinkTo": "حدد الشبكة للارتباط بها", + "createANewGrid": "قم بإنشاء شبكة جديدة" + } + }, + "plugins": { + "referencedBoard": "المجلس المشار إليه", + "referencedGrid": "الشبكة المشار إليها", + "autoGeneratorMenuItemName": "كاتب OpenAI", + "autoGeneratorTitleName": "OpenAI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...", + "autoGeneratorLearnMore": "يتعلم أكثر", + "autoGeneratorGenerate": "يولد", + "autoGeneratorHintText": "اسأل OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "لا يمكن الحصول على مفتاح OpenAI", + "smartEdit": "مساعدي الذكاء الاصطناعي", + "openAI": "OpenAI", + "smartEditFixSpelling": "أصلح التهجئة", + "warning": "⚠️ يمكن أن تكون استجابات الذكاء الاصطناعي غير دقيقة أو مضللة.", + "smartEditSummarize": "لخص", + "smartEditCouldNotFetchResult": "تعذر جلب النتيجة من OpenAI", + "smartEditCouldNotFetchKey": "تعذر جلب مفتاح OpenAI", + "smartEditDisabled": "قم بتوصيل OpenAI في الإعدادات", + "discardResponse": "هل تريد تجاهل استجابات الذكاء الاصطناعي؟", + "cover": { + "changeCover": "تبديل الغطاء", + "colors": "الألوان", + "images": "الصور", + "clearAll": "امسح الكل", + "abstract": "خلاصة", + "addCover": "أضف الغلاف", + "addLocalImage": "أضف الصورة المحلية", + "invalidImageUrl": "عنوان URL للصورة غير صالح", + "failedToAddImageToGallery": "فشل في إضافة الصورة إلى المعرض", + "enterImageUrl": "أدخل عنوان URL للصورة", + "add": "يضيف", + "back": "خلف", + "saveToGallery": "حفظ في المعرض", + "removeIcon": "إزالة الرمز", + "pasteImageUrl": "لصق عنوان URL للصورة", + "or": "أو", + "pickFromFiles": "اختر من الملفات", + "couldNotFetchImage": "تعذر جلب الصورة", + "imageSavingFailed": "فشل حفظ الصورة", + "addIcon": "إضافة أيقونة", + "coverRemoveAlert": "ستتم إزالته من الغلاف بعد حذفه.", + "alertDialogConfirmation": "هل أنت متأكد أنك تريد الاستمرار؟" + }, + "mathEquation": { + "addMathEquation": "أضف معادلة رياضية", + "editMathEquation": "تحرير المعادلة الرياضية" + } + } + }, + "board": { + "column": { + "create_new_card": "جديد" + }, + "menuName": "سبورة", + "referencedBoardPrefix": "نظرا ل" + }, + "calendar": { + "menuName": "تقويم", + "defaultNewCalendarTitle": "بدون عنوان", + "navigation": { + "today": "اليوم", + "jumpToday": "انتقل إلى اليوم", + "previousMonth": "الشهر الماضى", + "nextMonth": "الشهر القادم" + }, + "settings": { + "showWeekNumbers": "إظهار أرقام الأسبوع", + "showWeekends": "عرض عطلات نهاية الأسبوع", + "firstDayOfWeek": "اليوم الأول من الأسبوع", + "layoutDateField": "تقويم التخطيط بواسطة" + } + } +} diff --git a/frontend/appflowy_flutter/assets/translations/ca-ES.json b/frontend/appflowy_flutter/assets/translations/ca-ES.json new file mode 100644 index 0000000000..3c9c583e98 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/ca-ES.json @@ -0,0 +1,153 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Jo", + "welcomeText": "Benvingut a @:appName", + "githubStarText": "Preferit a Github", + "subscribeNewsletterText": "Subscriu-me al butlletí", + "letsGoButtonText": "Endavant", + "title": "Títol", + "signUp": { + "buttonText": "Registra't", + "title": "Registra't a @:appName", + "getStartedText": "Comencem", + "emptyPasswordError": "La contrasenya no pot ser buida", + "repeatPasswordEmptyError": "La contrasenya repetida no pot ser buida", + "unmatchedPasswordError": "Les contrasenyes no concorden", + "alreadyHaveAnAccount": "Ja tens un compte?", + "emailHint": "Correu electrònic", + "passwordHint": "Contrasenya", + "repeatPasswordHint": "Repeteix la contrasenya" + }, + "signIn": { + "loginTitle": "Inicia sessió a @:appName", + "loginButtonText": "Inicia sessió", + "buttonText": "Inicia sessió", + "forgotPassword": "Has oblidat la contrasenya?", + "emailHint": "Correu electrònic", + "passwordHint": "Contrasenya", + "dontHaveAnAccount": "No tens un compte?", + "repeatPasswordEmptyError": "La contrasenya repetida no pot ser buida", + "unmatchedPasswordError": "Les contrasenyes no concorden" + }, + "workspace": { + "create": "Crear un espai de treball", + "hint": "espai de treball", + "notFoundError": "No s'ha trobat l'espai de treball" + }, + "shareAction": { + "buttonText": "Compartir", + "workInProgress": "Pròximament", + "markdown": "Markdown", + "copyLink": "Copiar l'enllaç" + }, + "disclosureAction": { + "rename": "Canviar el nom", + "delete": "Eliminar", + "duplicate": "Duplicar" + }, + "blankPageTitle": "Pàgina en blanc", + "newPageText": "Nova pàgina", + "trash": { + "text": "Paperera", + "restoreAll": "Recuperar-ho tot", + "deleteAll": "Eliminar-ho tot", + "pageHeader": { + "fileName": "Nom del fitxer", + "lastModified": "Última modificació", + "created": "Creat" + } + }, + "deletePagePrompt": { + "text": "Aquest pàgina es troba a la paperera", + "restore": "Recuperar-la", + "deletePermanent": "Elimina-la" + }, + "dialogCreatePageNameHint": "Nom de la pàgina", + "questionBubble": { + "whatsNew": "Què hi ha de nou?", + "help": "Ajuda i Suport", + "debug": { + "name": "Informació de depuració", + "success": "S'ha copiat la informació de depuració!", + "fail": "No es pot copiar la informació de depuració" + } + }, + "menuAppHeader": { + "addPageTooltip": "Afegeix ràpidament una pàgina dins", + "defaultNewPageName": "Sense títol", + "renameDialog": "Canviar el nom" + }, + "toolbar": { + "undo": "Desfer", + "redo": "Refer", + "bold": "Negreta", + "italic": "Cursiva", + "underline": "Text subratllar", + "strike": "Ratllat", + "numList": "Llista numerada", + "bulletList": "Llista de punts", + "checkList": "Llista de comprovació", + "inlineCode": "Inserir codi", + "quote": "Bloc citat", + "header": "Capçalera", + "highlight": "Subratllar" + }, + "tooltip": { + "lightMode": "Canviar a mode clar", + "darkMode": "Canviar a mode fosc" + }, + "contactsPage": { + "title": "Contactes", + "whatsHappening": "Que passa aquesta setmana?", + "addContact": "Afegir un contacte", + "editContact": "Editar un contacte" + }, + "button": { + "OK": "OK", + "Cancel": "Cancel·lar", + "signIn": "Iniciar sessió", + "signOut": "Tancar sessió", + "complete": "Completar", + "save": "Guardar" + }, + "label": { + "welcome": "Benvingut!", + "firstName": "Nom", + "middleName": "Segon Nom", + "lastName": "Cognom", + "stepX": "Pas {X}" + }, + "oAuth": { + "err": { + "failedTitle": "No s'ha pogut connectar al teu compte.", + "failedMsg": "Assegureu-vos que heu completat el procés d'inici de sessió al vostre navegador." + }, + "google": { + "title": "Iniciar sessió amb Google", + "instruction1": "Per importar els vostres contactes de Google, haureu d'autoritzar aquesta aplicació mitjançant el vostre navegador web.", + "instruction2": "Copia aquest codi clicant la icona o seleccionant el text:", + "instruction3": "Navega al següent enllaç amb el teu navegador i insereix el codi anterior:", + "instruction4": "Pressiona el botó d'avall una vegada hagis completat el registre:" + } + }, + "settings": { + "title": "Configuració", + "menu": { + "appearance": "Aparença", + "language": "Idioma", + "open": "Obrir la configuració" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Mode Clar", + "dark": "Mode Fosc", + "system": "Adapt to System" + } + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/de-DE.json b/frontend/appflowy_flutter/assets/translations/de-DE.json new file mode 100644 index 0000000000..e1aa89897a --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/de-DE.json @@ -0,0 +1,159 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Ich", + "welcomeText": "Willkommen bei @:appName", + "githubStarText": "GitHub Star vergeben", + "subscribeNewsletterText": "Abonniere den Newsletter", + "letsGoButtonText": "Los geht's", + "title": "Titel", + "signUp": { + "buttonText": "Registrieren", + "title": "Registriere dich bei @:appName", + "getStartedText": "Erste Schritte", + "emptyPasswordError": "Passwort darf nicht leer sein", + "repeatPasswordEmptyError": "Passwortwiederholung darf nicht leer sein", + "unmatchedPasswordError": "Passwörter stimmen nicht überein", + "alreadyHaveAnAccount": "Bereits registriert?", + "emailHint": "E-Mail", + "passwordHint": "Passwort", + "repeatPasswordHint": "Wiederhole Passwort" + }, + "signIn": { + "loginTitle": "Bei @:appName einloggen", + "loginButtonText": "Anmelden", + "buttonText": "Anmelden", + "forgotPassword": "Passwort vergessen?", + "emailHint": "E-Mail", + "passwordHint": "Passwort", + "dontHaveAnAccount": "Du besitzt noch kein Konto?", + "repeatPasswordEmptyError": "Passwortwiederholung darf nicht leer sein", + "unmatchedPasswordError": "Passwörter stimmen nicht überein" + }, + "workspace": { + "create": "Arbeitsbereich erstellen", + "hint": "Arbeitsbereich", + "notFoundError": "Arbeitsbereich nicht gefunden" + }, + "shareAction": { + "buttonText": "Teilen", + "workInProgress": "Demnächst verfügbar", + "markdown": "Markdown", + "copyLink": "Link kopieren" + }, + "disclosureAction": { + "rename": "Umbenennen", + "delete": "Löschen", + "duplicate": "Duplizieren" + }, + "blankPageTitle": "Leere Seite", + "newPageText": "Neue Seite", + "trash": { + "text": "Papierkorb", + "restoreAll": "Alles wiederherstellen", + "deleteAll": "Alles löschen", + "pageHeader": { + "fileName": "Dateiname", + "lastModified": "Letzte Änderung", + "created": "Erstellt" + } + }, + "deletePagePrompt": { + "text": "Diese Seite ist im Papierkorb", + "restore": "Seite wiederherstellen", + "deletePermanent": "Dauerhaft löschen" + }, + "dialogCreatePageNameHint": "Seitenname", + "questionBubble": { + "whatsNew": "Was gibt es Neues?", + "help": "Hilfe & Support", + "debug": { + "name": "Debug-Informationen", + "success": "Debug-Informationen in die Zwischenablage kopiert!", + "fail": "Debug-Informationen können nicht in die Zwischenablage kopiert werden" + }, + "shortcuts": "Abkürzungen" + }, + "menuAppHeader": { + "addPageTooltip": "Schnell eine Seite innerhalb hinzufügen", + "defaultNewPageName": "Unbenannt", + "renameDialog": "Umbenennen" + }, + "toolbar": { + "undo": "Rückgängig", + "redo": "Wiederherstellen", + "bold": "Fett", + "italic": "Kursiv", + "underline": "Unterstreichen", + "strike": "Durchstreichen", + "numList": "Nummerierte Liste", + "bulletList": "Aufzählung", + "checkList": "Checkliste", + "inlineCode": "Inline-Code", + "quote": "Zitat", + "header": "Überschrift", + "highlight": "Hervorhebung", + "color": "Farbe" + }, + "tooltip": { + "lightMode": "In den hellen Modus wechseln", + "darkMode": "In den dunklen Modus wechseln", + "openAsPage": "Als Seite öffnen" + }, + "contactsPage": { + "title": "Kontakte", + "whatsHappening": "Was geschieht diese Woche?", + "addContact": "Kontakt hinzufügen", + "editContact": "Kontakt bearbeiten" + }, + "button": { + "OK": "OK", + "Cancel": "Abbrechen", + "signIn": "Anmelden", + "signOut": "Abmelden", + "complete": "Fertig", + "save": "Speichern" + }, + "label": { + "welcome": "Willkommen!", + "firstName": "Vorname", + "middleName": "Zweiter Vorname", + "lastName": "Nachname", + "stepX": "Schritt {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Keine Verbindung zu Ihrem Konto möglich.", + "failedMsg": "Bitte vergewissern Sie sich, dass Sie den Anmeldevorgang in Ihrem Browser abgeschlossen haben." + }, + "google": { + "title": "GOOGLE ANMELDUNG", + "instruction1": "Um Ihre Google-Kontakte zu importieren, müssen Sie diese Anwendung über Ihren Webbrowser autorisieren.", + "instruction2": "Kopieren Sie diesen Code in Ihre Zwischenablage, indem Sie auf das Symbol klicken oder den Text auswählen:", + "instruction3": "Rufen Sie den folgenden Link in Ihrem Webbrowser auf, und geben Sie den obigen Code ein:", + "instruction4": "Klicken Sie unten auf die Schaltfläche, wenn Sie die Anmeldung abgeschlossen haben:" + } + }, + "settings": { + "title": "Einstellungen", + "menu": { + "appearance": "Aussehen", + "language": "Sprache", + "open": "Einstellungen öffnen" + }, + "appearance": { + "lightLabel": "Heller Modus", + "darkLabel": "Dunkler Modus" + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + }, + "moreAction": { + "small": "klein", + "medium": "mittel", + "large": "groß", + "fontSize": "Schriftgröße", + "import": "Importieren" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json new file mode 100644 index 0000000000..9542927c1c --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -0,0 +1,487 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Me", + "welcomeText": "Welcome to @:appName", + "githubStarText": "Star on GitHub", + "subscribeNewsletterText": "Subscribe to Newsletter", + "letsGoButtonText": "Quick Start", + "title": "Title", + "youCanAlso": "You can also", + "and": "and", + "signUp": { + "buttonText": "Sign Up", + "title": "Sign Up to @:appName", + "getStartedText": "Get Started", + "emptyPasswordError": "Password can't be empty", + "repeatPasswordEmptyError": "Repeat password can't be empty", + "unmatchedPasswordError": "Repeat password is not the same as password", + "alreadyHaveAnAccount": "Already have an account?", + "emailHint": "Email", + "passwordHint": "Password", + "repeatPasswordHint": "Repeat password" + }, + "signIn": { + "loginTitle": "Login to @:appName", + "loginButtonText": "Login", + "loginAsGuestButtonText": "Get Started", + "buttonText": "Sign In", + "forgotPassword": "Forgot Password?", + "emailHint": "Email", + "passwordHint": "Password", + "dontHaveAnAccount": "Don't have an account?", + "repeatPasswordEmptyError": "Repeat password can't be empty", + "unmatchedPasswordError": "Repeat password is not the same as password" + }, + "workspace": { + "create": "Create workspace", + "hint": "workspace", + "notFoundError": "Workspace not found" + }, + "shareAction": { + "buttonText": "Share", + "workInProgress": "Coming soon", + "markdown": "Markdown", + "copyLink": "Copy Link" + }, + "moreAction": { + "small": "small", + "medium": "medium", + "large": "large", + "fontSize": "Font Size", + "import": "Import", + "moreOptions": "More options" + }, + "importPanel": { + "textAndMarkdown": "Text & Markdown", + "documentFromV010": "Document from v0.1.0", + "databaseFromV010": "Database from v0.1.0", + "csv": "CSV", + "database": "Database" + }, + "disclosureAction": { + "rename": "Rename", + "delete": "Delete", + "duplicate": "Duplicate" + }, + "blankPageTitle": "Blank page", + "newPageText": "New page", + "trash": { + "text": "Trash", + "restoreAll": "Restore All", + "deleteAll": "Delete All", + "pageHeader": { + "fileName": "File name", + "lastModified": "Last Modified", + "created": "Created" + } + }, + "deletePagePrompt": { + "text": "This page is in Trash", + "restore": "Restore page", + "deletePermanent": "Delete permanently" + }, + "dialogCreatePageNameHint": "Page name", + "questionBubble": { + "shortcuts": "Shortcuts", + "whatsNew": "What's new?", + "help": "Help & Support", + "markdown": "Markdown", + "debug": { + "name": "Debug Info", + "success": "Copied debug info to clipboard!", + "fail": "Unable to copy debug info to clipboard" + } + }, + "menuAppHeader": { + "addPageTooltip": "Quickly add a page inside", + "defaultNewPageName": "Untitled", + "renameDialog": "Rename" + }, + "toolbar": { + "undo": "Undo", + "redo": "Redo", + "bold": "Bold", + "italic": "Italic", + "underline": "Underline", + "strike": "Strikethrough", + "numList": "Numbered List", + "bulletList": "Bulleted List", + "checkList": "Check List", + "inlineCode": "Inline Code", + "quote": "Quote Block", + "header": "Header", + "highlight": "Highlight", + "color": "Color" + }, + "tooltip": { + "lightMode": "Switch to Light mode", + "darkMode": "Switch to Dark mode", + "openAsPage": "Open as a Page", + "addNewRow": "Add a new row", + "openMenu": "Click to open menu", + "dragRow": "Long press to reorder the row", + "viewDataBase": "View database", + "referencePage": "This {name} is referenced" + }, + "sideBar": { + "closeSidebar": "Close side bar", + "openSidebar": "Open side bar" + }, + "notifications": { + "export": { + "markdown": "Exported Note To Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "Contacts", + "whatsHappening": "What's happening this week?", + "addContact": "Add Contact", + "editContact": "Edit Contact" + }, + "button": { + "OK": "OK", + "Done": "Done", + "Cancel": "Cancel", + "signIn": "Sign In", + "signOut": "Sign Out", + "complete": "Complete", + "save": "Save", + "generate": "Generate", + "esc": "ESC", + "keep": "Keep", + "tryAgain": "Try again", + "discard": "Discard", + "replace": "Replace", + "insertBelow": "Insert Below" + }, + "label": { + "welcome": "Welcome!", + "firstName": "First Name", + "middleName": "Middle Name", + "lastName": "Last Name", + "stepX": "Step {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Unable to connect to your account.", + "failedMsg": "Please make sure you've completed the sign-in process in your browser." + }, + "google": { + "title": "GOOGLE SIGN-IN", + "instruction1": "In order to import your Google Contacts, you'll need to authorize this application using your web browser.", + "instruction2": "Copy this code to your clipboard by clicking the icon or selecting the text:", + "instruction3": "Navigate to the following link in your web browser, and enter the above code:", + "instruction4": "Press the button below when you've completed signup:" + } + }, + "settings": { + "title": "Settings", + "menu": { + "appearance": "Appearance", + "language": "Language", + "user": "User", + "files": "Files", + "open": "Open Settings" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Light Mode", + "dark": "Dark Mode", + "system": "Adapt to System" + }, + "theme": "Theme" + }, + "files": { + "copy": "Copy", + "defaultLocation": "Read files and data storage location", + "exportData": "Export your data", + "doubleTapToCopy": "Double tap to copy the path", + "restoreLocation": "Restore to AppFlowy default path", + "customizeLocation": "Open another folder", + "restartApp": "Please restart app for the changes to take effect.", + "exportDatabase": "Export database", + "selectFiles": "Select the files that need to be export", + "selectAll": "Select all", + "deselectAll": "Deselect all", + "createNewFolder": "Create a new folder", + "createNewFolderDesc": "Tell us where you want to store your data", + "defineWhereYourDataIsStored": "Define where your data is stored", + "open": "Open", + "openFolder": "Open an existing folder", + "openFolderDesc": "Read and write it to your existing AppFlowy folder", + "folderHintText": "folder name", + "location": "Creating a new folder", + "locationDesc": "Pick a name for your AppFlowy data folder", + "browser": "Browse", + "create": "Create", + "set": "Set", + "folderPath": "Path to store your folder", + "locationCannotBeEmpty": "Path cannot be empty", + "pathCopiedSnackbar": "File storage path copied to clipboard!", + "changeLocationTooltips": "Change the data directory", + "change": "Change", + "openLocationTooltips": "Open another data directory", + "openCurrentDataFolder": "Open current data directory", + "recoverLocationTooltips": "Reset to AppFlowy's default data directory", + "exportFileSuccess": "Export file successfully!", + "exportFileFail": "Export file failed!", + "export": "Export" + }, + "user": { + "name": "Name", + "icon": "Icon", + "selectAnIcon": "Select an icon", + "pleaseInputYourOpenAIKey": "please input your OpenAI key" + } + }, + "grid": { + "deleteView": "Are you sure you want to delete this view?", + "createView": "New", + "settings": { + "filter": "Filter", + "sort": "Sort", + "sortBy": "Sort by", + "Properties": "Properties", + "group": "Group", + "addFilter": "Add Filter", + "deleteFilter": "Delete filter", + "filterBy": "Filter by...", + "typeAValue": "Type a value...", + "layout": "Layout", + "databaseLayout": "Layout" + }, + "textFilter": { + "contains": "Contains", + "doesNotContain": "Does not contain", + "endsWith": "Ends with", + "startWith": "Starts with", + "is": "Is", + "isNot": "Is not", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty", + "choicechipPrefix": { + "isNot": "Not", + "startWith": "Starts with", + "endWith": "Ends with", + "isEmpty": "is empty", + "isNotEmpty": "is not empty" + } + }, + "checkboxFilter": { + "isChecked": "Checked", + "isUnchecked": "Unchecked", + "choicechipPrefix": { + "is": "is" + } + }, + "checklistFilter": { + "isComplete": "is complete", + "isIncomplted": "is incomplete" + }, + "singleSelectOptionFilter": { + "is": "Is", + "isNot": "Is not", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty" + }, + "multiSelectOptionFilter": { + "contains": "Contains", + "doesNotContain": "Does not contain", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty" + }, + "field": { + "hide": "Hide", + "insertLeft": "Insert Left", + "insertRight": "Insert Right", + "duplicate": "Duplicate", + "delete": "Delete", + "textFieldName": "Text", + "checkboxFieldName": "Checkbox", + "dateFieldName": "Date", + "updatedAtFieldName": "Last modified time", + "createdAtFieldName": "Created time", + "numberFieldName": "Numbers", + "singleSelectFieldName": "Select", + "multiSelectFieldName": "Multiselect", + "urlFieldName": "URL", + "checklistFieldName": "Checklist", + "numberFormat": "Number format", + "dateFormat": "Date format", + "includeTime": "Include time", + "dateFormatFriendly": "Month Day, Year", + "dateFormatISO": "Year-Month-Day", + "dateFormatLocal": "Month/Day/Year", + "dateFormatUS": "Year/Month/Day", + "dateFormatDayMonthYear": "Day/Month/Year", + "timeFormat": "Time format", + "invalidTimeFormat": "Invalid format", + "timeFormatTwelveHour": "12 hour", + "timeFormatTwentyFourHour": "24 hour", + "addSelectOption": "Add an option", + "optionTitle": "Options", + "addOption": "Add option", + "editProperty": "Edit property", + "newProperty": "New property", + "deleteFieldPromptMessage": "Are you sure? This property will be deleted" + }, + "sort": { + "ascending": "Ascending", + "descending": "Descending", + "deleteSort": "Delete sort", + "addSort": "Add sort" + }, + "row": { + "duplicate": "Duplicate", + "delete": "Delete", + "textPlaceholder": "Empty", + "copyProperty": "Copied property to clipboard", + "count": "Count", + "newRow": "New row", + "action": "Action" + }, + "selectOption": { + "create": "Create", + "purpleColor": "Purple", + "pinkColor": "Pink", + "lightPinkColor": "Light Pink", + "orangeColor": "Orange", + "yellowColor": "Yellow", + "limeColor": "Lime", + "greenColor": "Green", + "aquaColor": "Aqua", + "blueColor": "Blue", + "deleteTag": "Delete tag", + "colorPanelTitle": "Colors", + "panelTitle": "Select an option or create one", + "searchOption": "Search for an option" + }, + "checklist": { + "panelTitle": "Add an item" + }, + "menuName": "Grid", + "referencedGridPrefix": "View of" + }, + "document": { + "menuName": "Document", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "Select a Board to link to", + "createANewBoard": "Create a new Board" + }, + "grid": { + "selectAGridToLinkTo": "Select a Grid to link to", + "createANewGrid": "Create a new Grid" + }, + "calendar": { + "selectACalendarToLinkTo": "Select a Calendar to link to", + "createANewCalendar": "Create a new Calendar" + } + }, + "plugins": { + "referencedBoard": "Referenced Board", + "referencedGrid": "Referenced Grid", + "referencedCalendar": "Referenced Calendar", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...", + "autoGeneratorLearnMore": "Learn more", + "autoGeneratorGenerate": "Generate", + "autoGeneratorHintText": "Ask OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key", + "autoGeneratorRewrite": "Rewrite", + "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 OpenAI", + "smartEditCouldNotFetchKey": "Could not fetch OpenAI key", + "smartEditDisabled": "Connect OpenAI in Settings", + "discardResponse": "Do you want to discard the AI responses?", + "cover": { + "changeCover": "Change Cover", + "colors": "Colors", + "images": "Images", + "clearAll": "Clear All", + "abstract": "Abstract", + "addCover": "Add Cover", + "addLocalImage": "Add local image", + "invalidImageUrl": "Invalid image URL", + "failedToAddImageToGallery": "Failed to add image to gallery", + "enterImageUrl": "Enter image URL", + "add": "Add", + "back": "Back", + "saveToGallery": "Save to gallery", + "removeIcon": "Remove Icon", + "pasteImageUrl": "Paste image URL", + "or": "OR", + "pickFromFiles": "Pick from files", + "couldNotFetchImage": "Could not fetch image", + "imageSavingFailed": "Image Saving Failed", + "addIcon": "Add Icon", + "coverRemoveAlert": "It will be removed from cover after it is deleted.", + "alertDialogConfirmation": "Are you sure, you want to continue?" + }, + "mathEquation": { + "addMathEquation": "Add Math Equation", + "editMathEquation": "Edit Math Equation" + }, + "optionAction": { + "click": "Click", + "toOpenMenu": " to open menu", + "delete": "Delete", + "duplicate": "Duplicate", + "turnInto": "Turn into", + "moveUp": "Move up", + "moveDown": "Move down", + "color": "Color", + "align": "Align", + "left": "Left", + "center": "Center", + "right": "Right", + "defaultColor": "Default" + } + } + }, + "board": { + "column": { + "create_new_card": "New" + }, + "menuName": "Board", + "referencedBoardPrefix": "View of" + }, + "calendar": { + "menuName": "Calendar", + "defaultNewCalendarTitle": "Untitled", + "navigation": { + "today": "Today", + "jumpToday": "Jump to Today", + "previousMonth": "Previous Month", + "nextMonth": "Next Month" + }, + "settings": { + "showWeekNumbers": "Show week numbers", + "showWeekends": "Show weekends", + "firstDayOfWeek": "Start week on", + "layoutDateField": "Layout calendar by", + "noDateTitle": "No Date", + "noDateHint": "Unscheduled events will show up here", + "clickToAdd": "Click to add to the calendar", + "name": "Calendar layout" + }, + "referencedCalendarPrefix": "View of" + }, + "errorDialog": { + "title": "AppFlowy Error", + "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", + "github": "View on GitHub" + } +} diff --git a/frontend/appflowy_flutter/assets/translations/es-VE.json b/frontend/appflowy_flutter/assets/translations/es-VE.json new file mode 100644 index 0000000000..91a59858a9 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/es-VE.json @@ -0,0 +1,231 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Mi", + "welcomeText": "Bienvenido a @:appName", + "githubStarText": "Favorito en GitHub", + "subscribeNewsletterText": "Suscribir al boletín", + "letsGoButtonText": "Vamos", + "title": "Título", + "signUp": { + "buttonText": "Registrar", + "title": "Registrar en @:appName", + "getStartedText": "Empezar", + "emptyPasswordError": "La contraseña no puede estar en blanco", + "repeatPasswordEmptyError": "La contraseña no puede estar en blanco", + "unmatchedPasswordError": "Las contraseñas no coinciden", + "alreadyHaveAnAccount": "¿Posee credenciales?", + "emailHint": "Correo", + "passwordHint": "Contraseña", + "repeatPasswordHint": "Repetir contraseña" + }, + "signIn": { + "loginTitle": "Ingresa a @:appName", + "loginButtonText": "Ingresar", + "buttonText": "Ingresar", + "forgotPassword": "¿Olvidó su contraseña?", + "emailHint": "Correo", + "passwordHint": "Contraseña", + "dontHaveAnAccount": "¿No posee credenciales?", + "repeatPasswordEmptyError": "La contraseña no puede estar en blanco", + "unmatchedPasswordError": "Las contraseñas no coinciden" + }, + "workspace": { + "create": "Crear espacio de trabajo", + "hint": "Espacio de trabajo", + "notFoundError": "Espacio de trabajo no encontrado" + }, + "shareAction": { + "buttonText": "Compartir", + "workInProgress": "Próximamente", + "markdown": "Marcador", + "copyLink": "Copiar enlace" + }, + "disclosureAction": { + "rename": "Renombrar", + "delete": "Eliminar", + "duplicate": "Duplicar" + }, + "blankPageTitle": "Página en blanco", + "newPageText": "Nueva página", + "trash": { + "text": "Papelera", + "restoreAll": "Recuperar todo", + "deleteAll": "Eliminar todo", + "pageHeader": { + "fileName": "Nombre de archivo", + "lastModified": "Última modificación", + "created": "Creado" + } + }, + "deletePagePrompt": { + "text": "Esta página está en la Papelera", + "restore": "Recuperar página", + "deletePermanent": "Eliminar permanentemente" + }, + "dialogCreatePageNameHint": "Nombre de página", + "questionBubble": { + "whatsNew": "¿Qué hay de nuevo?", + "help": "Ayuda y Soporte", + "debug": { + "name": "Información de depuración", + "success": "¡Información copiada!", + "fail": "No fue posible copiar la información" + } + }, + "menuAppHeader": { + "addPageTooltip": "Inserta una página", + "defaultNewPageName": "Sin Título", + "renameDialog": "Renombrar" + }, + "toolbar": { + "undo": "Deshacer", + "redo": "Rehacer", + "bold": "Negrita", + "italic": "Cursiva", + "underline": "Subrayado", + "strike": "Tachado", + "numList": "Lista numerada", + "bulletList": "Lista con viñetas", + "checkList": "Lista de verificación", + "inlineCode": "Código embebido", + "quote": "Cita", + "header": "Título", + "highlight": "Resaltado", + "color": "Color" + }, + "tooltip": { + "lightMode": "Cambiar a modo Claro", + "darkMode": "Cambiar a modo Oscuro" + }, + "notifications": { + "export": { + "markdown": "Nota exportada a Markdown", + "path": "Documentos/flowy" + } + }, + "contactsPage": { + "title": "Contactos", + "whatsHappening": "¿Qué está pasando esta semana?", + "addContact": "Agregar Contacto", + "editContact": "Editar Contacto" + }, + "button": { + "OK": "OK", + "Cancel": "Cancelar", + "signIn": "Ingresar", + "signOut": "Salir", + "complete": "Completar", + "save": "Guardar" + }, + "label": { + "welcome": "¡Bienvenido!", + "firstName": "Primer nombre", + "middleName": "Segundo nombre", + "lastName": "Apellido", + "stepX": "Paso {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Imposible conectarse con sus credenciales.", + "failedMsg": "Por favor asegurese haber completado el proceso de ingreso en su navegador." + }, + "google": { + "title": "Ingresar con Google", + "instruction1": "Para importar sus contactos de Google, debe autorizar esta aplicación usando su navegador web.", + "instruction2": "Copie este código al presionar el icono o al seleccionar el texto:", + "instruction3": "Navege al siguiente enlace en su navegador web, e ingrese el código anterior:", + "instruction4": "Presione el botón de abajo cuando haya completado su registro:" + } + }, + "settings": { + "title": "Ajustes", + "menu": { + "appearance": "Apariencia", + "language": "Lenguaje", + "open": "Abrir ajustes" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Modo Claro", + "dark": "Modo Oscuro", + "system": "Adapt to System" + } + } + }, + "grid": { + "settings": { + "filter": "Filtrar", + "sortBy": "Ordenar por", + "Properties": "Propiedades" + }, + "field": { + "hide": "Ocultar", + "insertLeft": "Insertar a la Izquierda", + "insertRight": "Insertar a la Derecha", + "duplicate": "Duplicar", + "delete": "Eliminar", + "textFieldName": "Texto", + "checkboxFieldName": "Casilla de verificación", + "dateFieldName": "Fecha", + "numberFieldName": "Números", + "singleSelectFieldName": "Seleccionar", + "multiSelectFieldName": "Selección múltiple", + "urlFieldName": "URL", + "numberFormat": "Formato numérico", + "dateFormat": "Formato de fecha", + "includeTime": "Incluir tiempo", + "dateFormatFriendly": "Mes Día, Año", + "dateFormatISO": "Año-Mes-Día", + "dateFormatLocal": "Mes/Día/Año", + "dateFormatUS": "Año/Mes/Día", + "timeFormat": "Formato de tiempo", + "invalidTimeFormat": "Formato de tiempo inválido", + "timeFormatTwelveHour": "12 horas", + "timeFormatTwentyFourHour": "24 horas", + "addSelectOption": "Añadir una opción", + "optionTitle": "Opciones", + "addOption": "Añadir opción", + "editProperty": "Editar propiedad" + }, + "row": { + "duplicate": "Duplicar", + "delete": "Eliminar", + "textPlaceholder": "Vacío", + "copyProperty": "Propiedad copiada al portapapeles" + }, + "selectOption": { + "create": "Crear", + "purpleColor": "Morado", + "pinkColor": "Rosa", + "lightPinkColor": "Rosa Claro", + "orangeColor": "Naranja", + "yellowColor": "Amarillo", + "limeColor": "Lima", + "greenColor": "Verde", + "aquaColor": "Agua", + "blueColor": "Azul", + "deleteTag": "Borrar etiqueta", + "colorPanelTitle": "Colores", + "panelTitle": "Selecciona una opción o crea una", + "searchOption": "Buscar una opción" + }, + "menuName": "Cuadrícula" + }, + "document": { + "menuName": "Documento", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + } + }, + "sideBar": { + "openSidebar": "Abrir panel lateral", + "closeSidebar": "Cerrar panel lateral" + }, + "board": { + "column": { + "create_new_card": "Nuevo" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/eu-ES.json b/frontend/appflowy_flutter/assets/translations/eu-ES.json new file mode 100644 index 0000000000..4e6a770a0f --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/eu-ES.json @@ -0,0 +1,333 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Ni", + "welcomeText": "Ongietorri @:appName -ra", + "githubStarText": "Izarra GitHub-en", + "subscribeNewsletterText": "Harpidetu buletinera", + "letsGoButtonText": "Hasi", + "title": "Izenburua", + "signUp": { + "buttonText": "Izena eman", + "title": "Izena eman @:appName -ra", + "getStartedText": "Hasi", + "emptyPasswordError": "Pasahitzak ezin du hutsik egon", + "repeatPasswordEmptyError": "Pasahitz errepikapenak ezin du hutsik egon", + "unmatchedPasswordError": "Pasahitz errepikapena ez da berdina", + "alreadyHaveAnAccount": "Kontu bat duzu jada?", + "emailHint": "Emaila", + "passwordHint": "Pasahitza", + "repeatPasswordHint": "Pasahitza errepikatu" + }, + "signIn": { + "loginTitle": "Hasi saioa @:appName -n", + "loginButtonText": "Hasi saioa", + "buttonText": "Sartu", + "forgotPassword": "Pasahitza ahaztu duzu?", + "emailHint": "Emaila", + "passwordHint": "Pasahitza", + "dontHaveAnAccount": "Ez daukazu konturik?", + "repeatPasswordEmptyError": "Pasahitz errepikapenak ezin du hutsik egon", + "unmatchedPasswordError": "Pasahitz errepikapena ez da berdina" + }, + "workspace": { + "create": "Lan-eremua", + "hint": "lan-eremua", + "notFoundError": "Lan-eremurik ez da aurkitu" + }, + "shareAction": { + "buttonText": "Konpartitu", + "workInProgress": "Laister", + "markdown": "Markdown", + "copyLink": "Esteka kopiatu" + }, + "moreAction": { + "small": "txikia", + "medium": "ertaina", + "large": "handia", + "fontSize": "Letra tamaina" + }, + "disclosureAction": { + "rename": "Izena aldatu", + "delete": "Ezabatu", + "duplicate": "Duplikatu" + }, + "blankPageTitle": "Orri zuria", + "newPageText": "Orri berria", + "trash": { + "text": "Zaborrontzia", + "restoreAll": "Guztia berreskuratu", + "deleteAll": "Guztia ezabatu", + "pageHeader": { + "fileName": "Fitxategi izena", + "lastModified": "Azken aldaketa", + "created": "Sortua" + } + }, + "deletePagePrompt": { + "text": "Orri hau zaborrontzian dago", + "restore": "Orria berreskuratu", + "deletePermanent": "Betirako ezabatu" + }, + "dialogCreatePageNameHint": "Orriaren izena", + "questionBubble": { + "whatsNew": "Ze berri?", + "help": "Laguntza", + "debug": { + "name": "Debug informazioa", + "success": "Debug informazioa kopiatu da!", + "fail": "Ezin izan da debug informazioa kopiatu" + } + }, + "menuAppHeader": { + "addPageTooltip": "Gehitu orri bat", + "defaultNewPageName": "Izenbururik ez", + "renameDialog": "Izena aldatu" + }, + "toolbar": { + "undo": "Desegin", + "redo": "Berregin", + "bold": "Lodia", + "italic": "Etzana", + "underline": "Azpimarratua", + "strike": "Markatua", + "numList": "Zembakidun zerrenda", + "bulletList": "Buletetako zerrenda", + "checkList": "Egiaztapen zerrenda", + "inlineCode": "Lerroko kodea", + "quote": "Aipamena", + "header": "Goiburua", + "highlight": "Nabarmendu", + "color": "Kolorea" + }, + "tooltip": { + "lightMode": "Modu argira aldatu", + "darkMode": "Modu ilunera aldatu", + "openAsPage": "Orri gisa ireki", + "addNewRow": "Ilara berri bat gehitu", + "openMenu": "Egin klik menua irekitzeko" + }, + "sideBar": { + "closeSidebar": "Alboko barra itxi", + "openSidebar": "Alboko barra ireki" + }, + "notifications": { + "export": { + "markdown": "Oharra markdownera esportatuta", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "Kontaktuak", + "whatsHappening": "Ze berri aste honetan?", + "addContact": "Kontaktua gehitu", + "editContact": "Kontaktua editatu" + }, + "button": { + "OK": "OK", + "Cancel": "Ezteztatu", + "signIn": "Saioa hasi", + "signOut": "Saioa itxi", + "complete": "Burututa", + "save": "Gorde" + }, + "label": { + "welcome": "Ongi etorri!", + "firstName": "Izena", + "middleName": "Bigarren izena", + "lastName": "Abizena", + "stepX": "{X}. pausoa" + }, + "oAuth": { + "err": { + "failedTitle": "Ezin izan da kontura sartu.", + "failedMsg": "Mesedez, ziurtatu zure arakatzailean saioa hasteko prozesua amaitu duzula." + }, + "google": { + "title": "GOOGLE SAIOA HASI", + "instruction1": "Zure Google Kontaktuak inportatzeko, zure web arakatzailea erabiliz aplikazio hau baimendu beharko duzu.", + "instruction2": "Kopiatu kode hau ikonoan klik eginez edo testua hautatuz:", + "instruction3": "Nabigatu zure web arakatzailean esteka honetara eta idatzi goiko kodea:", + "instruction4": "Sakatu beheko botoia erregistroa amaitzean:" + } + }, + "settings": { + "title": "Ezarpenak", + "menu": { + "appearance": "Itxura", + "language": "Hizkuntza", + "user": "Erabiltzailea", + "files": "Fitxategiak", + "open": "Ezarpenak ireki" + }, + "appearance": { + "themeMode": { + "label": "Itxura modua", + "light": "Modu argia", + "dark": "Modu iluna", + "system": "Zure sistemara moldatu" + }, + "theme": "Itxura" + }, + "files": { + "defaultLocation": "Non gordetzen diren zure datuak", + "doubleTapToCopy": "Sakatu birritan bidea kopiatzeko", + "restoreLocation": "Berrezarri AppFlowy-ren biden lehenetsira", + "customizeLocation": "Beste karpeta bat ireki", + "restartApp": "Mesedez, berrabiarazi aplikazioa aldaketak indarrean egon daitezen.", + "exportDatabase": "Datubasea exportatu", + "selectFiles": "Aukeratu exportatu nahi dituzun fitxategiak", + "createNewFolder": "Karpeta berri bat sortu", + "createNewFolderDesc": "Non nahi dituzu datuak gorde ...", + "open": "Oreki", + "openFolder": "Ireki karpeta bat", + "openFolderDesc": "Irakurri eta idatzi zure AppFlowy karpetan...", + "folderHintText": "karpetaren izena", + "location": "Karpeta berria sortzen", + "locationDesc": "Aukeratu izen bat AppFlowy datuen karpetarako", + "browser": "Bilatu", + "create": "Sortu", + "folderPath": "Zure karpeta gordetzeko bidea", + "locationCannotBeEmpty": "Bideak ezin du hutsa egon" + } + }, + "grid": { + "settings": { + "filter": "Filtroa", + "sort": "Ordenatu", + "sortBy": "Ordenatu honekiko", + "Properties": "Propietateak", + "group": "Taldea", + "addFilter": "Gehitu iragazkia", + "deleteFilter": "Ezabatu iragazkia", + "filterBy": "Iragazi arabera...", + "typeAValue": "Idatzi balio bat..." + }, + "textFilter": { + "contains": "Dauka", + "doesNotContain": "Ez dauka", + "endsWith": "Honez amaitzen da", + "startWith": "Honez hasten da", + "is": "da", + "isNot": "Ez da", + "isEmpty": "Hutsa dago", + "isNotEmpty": "Ez dago hutsik", + "choicechipPrefix": { + "isNot": "Ez da", + "startWith": "Honez hasten da", + "endWith": "Honez amaitzen da", + "isEmpty": "hutsik dago", + "isNotEmpty": "ez dago hutsik" + } + }, + "checkboxFilter": { + "isChecked": "Egiaztatuta", + "isUnchecked": "Desmarkatua", + "choicechipPrefix": { + "da": "da" + } + }, + "checklistFilter": { + "isComplete": "osatu da", + "isIncomplted": "osatu gabe dago" + }, + "singleSelectOptionFilter": { + "is": "da", + "isNot": "Ez da", + "isEmpty": "Hutsa dago", + "isNotEmpty": "Ez dago hutsik" + }, + "multiSelectOptionFilter": { + "contains": "Duen", + "doesNotContain": "Ez dauka", + "isEmpty": "Hutsa dago", + "isNotEmpty": "Ez dago hutsik" + }, + "field": { + "hide": "Ezkutatu", + "insertLeft": "Txertatu ezkerrera", + "insertRight": "Txertatu eskuinera", + "duplicate": "Bikoiztu", + "delete": "Ezabatu", + "textFieldName": "Testua", + "checkboxFieldName": "Markatu laukia", + "dateFieldName": "Data", + "numberFieldName": "Zenbakiak", + "singleSelectFieldName": "Hautatu", + "multiSelectFieldName": "Multi-hautaketa", + "urlFieldName": "URL", + "checklistFieldName": "Kontrol zerrenda", + "numberFormat": "Zenbaki formatua", + "dateFormat": "Data formatua", + "includeTime": "Sartu ordua", + "dateFormatFriendly": "Hilabete Eguna, Urtea", + "dateFormatISO": "Urtea-Hilabetea-Eguna", + "dateFormatLocal": "Hilabetea/Eguna/Urtea", + "dateFormatUS": "Urtea/Hilabetea/Eguna", + "timeFormat": "Denboraren formatua", + "invalidTimeFormat": "Formatu baliogabea", + "timeFormatTwelveHour": "12 ordu", + "timeFormatTwentyFourHour": "24 ordu", + "addSelectOption": "Gehitu aukera bat", + "optionTitle": "Aukerak", + "addOption": "Gehitu aukera", + "editProperty": "Editatu propietatea", + "newProperty": "Zutabe berria", + "deleteFieldPromptMessage": "Ziur al zaude? Propietate hau ezabatu egingo da" + }, + "sort": { + "ascending": "Gorarantz", + "descending": "Jaisten", + "deleteSort": "Ezabatu ordena", + "addSort": "Gehitu ordenatu" + }, + "row": { + "duplicate": "Bikoiztu", + "delete": "Ezabatu", + "textPlaceholder": "Hutsik", + "copyProperty": "Propietatea arbelean kopiatu da", + "count": "Kontatu", + "newRow": "Errenkada berria" + }, + "selectOption": { + "create": "Sortu", + "purpleColor": "Purple", + "pinkColor": "Rosa", + "lightPinkColor": "Arrosa argia", + "orangeColor": "Laranja", + "yellowColor": "Horia", + "limeColor": "Lima", + "greenColor": "Berdea", + "aquaColor": "Aqua", + "blueColor": "Urdina", + "deleteTag": "Ezabatu etiketa", + "colorPanelTitle": "Koloreak", + "panelTitle": "Hautatu aukera bat edo sortu bat", + "searchOption": "Aukera bat bilatu" + }, + "checklist": { + "panelTitle": "Gehitu elementu bat" + }, + "menuName": "Sareta" + }, + "document": { + "menuName": "Dokumentua", + "data": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + } + }, + "board": { + "column": { + "create_new_card": "Berria" + } + }, + "calendar": { + "menuName": "Egutegia", + "navigation": { + "today": "Gaur", + "jumpToday": "Gaurko egunera salto egin", + "previousMonth": "Aurreko hilabetea", + "nextMonth": "Hurrengo hilabetea" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/fr-CA.json b/frontend/appflowy_flutter/assets/translations/fr-CA.json new file mode 100644 index 0000000000..829f2a7d89 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/fr-CA.json @@ -0,0 +1,153 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Moi", + "welcomeText": "Bienvenue sur @:appName", + "githubStarText": "Favoriser sur GitHub", + "subscribeNewsletterText": "Abonnez-vous à notre courriel", + "letsGoButtonText": "Allons-y", + "title": "Titre", + "signUp": { + "buttonText": "S'inscrire", + "title": "S'inscrire à @:appName", + "getStartedText": "Commencer", + "emptyPasswordError": "Vous n'avez pas saisi votre mot de passe", + "repeatPasswordEmptyError": "Mot de passe ne doit pas être vide", + "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", + "alreadyHaveAnAccount": "Avez-vous déjà un compte?", + "emailHint": "courriel", + "passwordHint": "Mot de passe", + "repeatPasswordHint": "Ressaisir votre mot de passe" + }, + "signIn": { + "loginTitle": "Connexion à @:appName", + "loginButtonText": "Connexion", + "buttonText": "Se connecter", + "forgotPassword": "Mot de passe oublié?", + "emailHint": "courriel", + "passwordHint": "Mot de passe", + "dontHaveAnAccount": "Vous n'avez pas de compte?", + "repeatPasswordEmptyError": "Mot de passe ne doit pas être vide", + "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques" + }, + "workspace": { + "create": "Créer un espace de travail", + "hint": "Espace de travail", + "notFoundError": "Espace de travail introuvable" + }, + "shareAction": { + "buttonText": "Partager", + "workInProgress": "Bientôt disponible", + "markdown": "Markdown", + "copyLink": "Copier le lien" + }, + "disclosureAction": { + "rename": "Renommer", + "delete": "Supprimer", + "duplicate": "Dupliquer" + }, + "blankPageTitle": "Page vierge", + "newPageText": "Nouvelle page", + "trash": { + "text": "Corbeille", + "restoreAll": "Tout récupérer", + "deleteAll": "Tout supprimer", + "pageHeader": { + "fileName": "Nom de fichier", + "lastModified": "Dernière modification", + "created": "Créé" + } + }, + "deletePagePrompt": { + "text": "Cette page est dans la corbeille", + "restore": "Récupérer la page", + "deletePermanent": "Supprimer définitivement" + }, + "dialogCreatePageNameHint": "Nom de la page", + "questionBubble": { + "whatsNew": "Nouveautés", + "help": "Aide et Support Technique", + "debug": { + "name": "Infos du système", + "success": "Info copié!", + "fail": "Impossible de copier l'info" + } + }, + "menuAppHeader": { + "addPageTooltip": "Ajouter une page", + "defaultNewPageName": "Sans titre", + "renameDialog": "Renommer" + }, + "toolbar": { + "undo": "Annuler", + "redo": "Rétablir", + "bold": "Gras", + "italic": "Italique", + "underline": "Souligner", + "strike": "Barré", + "numList": "Liste numérotée", + "bulletList": "Liste à puces", + "checkList": "Liste de contrôle", + "inlineCode": "Code en ligne", + "quote": "Citation", + "header": "En-tête", + "highlight": "Surligner" + }, + "tooltip": { + "lightMode": "Passer en mode clair", + "darkMode": "Passer en mode sombre" + }, + "contactsPage": { + "title": "Contacts", + "whatsHappening": "Quoi de neuf?", + "addContact": "Ajouter un contact", + "editContact": "Modifier le contact" + }, + "button": { + "OK": "OK", + "Cancel": "Annuler", + "signIn": "Se connecter", + "signOut": "Se déconnecter", + "complete": "Achevé", + "save": "Sauvegarder" + }, + "label": { + "welcome": "Bienvenue!", + "firstName": "Prénom", + "middleName": "Deuxième nom", + "lastName": "Nom de famille", + "stepX": "Étape {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Incapable de se connecter à votre compte.", + "failedMsg": "SVP vous assurrez d'avoir complèté le processus d'enregistrement dans votre fureteur." + }, + "google": { + "title": "S'identifier avec Google", + "instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur Web.", + "instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:", + "instruction3": "Accédez au lien suivant dans votre navigateur Web et saisissez le code ci-dessus:", + "instruction4": "Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription:" + } + }, + "settings": { + "title": "Paramètres", + "menu": { + "appearance": "Apparence", + "language": "Langue", + "open": "Ouvrir les paramètres" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Mode clair", + "dark": "Mode sombre", + "system": "Adapt to System" + } + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/fr-FR.json b/frontend/appflowy_flutter/assets/translations/fr-FR.json new file mode 100644 index 0000000000..5fa0390c19 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/fr-FR.json @@ -0,0 +1,239 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Moi", + "welcomeText": "Bienvenue sur @:appName", + "githubStarText": "Favoriser sur GitHub", + "subscribeNewsletterText": "S'inscrire à la Newsletter", + "letsGoButtonText": "Allons-y", + "title": "Titre", + "signUp": { + "buttonText": "S'inscrire", + "title": "Inscrivez-vous sur @:appName", + "getStartedText": "Commencer", + "emptyPasswordError": "Vous n'avez pas saisi votre mot de passe", + "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", + "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", + "alreadyHaveAnAccount": "Avez-vous déjà un compte ?", + "emailHint": "Email", + "passwordHint": "Mot de passe", + "repeatPasswordHint": "Ressaisir votre mot de passe" + }, + "signIn": { + "loginTitle": "Connexion à @:appName", + "loginButtonText": "Connexion", + "buttonText": "Se connecter", + "forgotPassword": "Mot de passe oublié ?", + "emailHint": "Email", + "passwordHint": "Mot de passe", + "dontHaveAnAccount": "Vous n'avez pas encore créé votre compte ?", + "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", + "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques" + }, + "workspace": { + "create": "Créer un espace de travail", + "hint": "Espace de travail", + "notFoundError": "Espace de travail introuvable" + }, + "shareAction": { + "buttonText": "Partager", + "workInProgress": "Bientôt disponible", + "markdown": "Markdown", + "copyLink": "Copier le lien" + }, + "disclosureAction": { + "rename": "Renommer", + "delete": "Supprimer", + "duplicate": "Dupliquer" + }, + "blankPageTitle": "Page vierge", + "newPageText": "Nouvelle page", + "trash": { + "text": "Corbeille", + "restoreAll": "Restaurer tout", + "deleteAll": "Supprimer tout", + "pageHeader": { + "fileName": "Nom de fichier", + "lastModified": "Dernière modification", + "created": "Créé" + } + }, + "deletePagePrompt": { + "text": "Cette page a été supprimée, vous pouvez la retrouver dans la corbeille", + "restore": "Restaurer la page", + "deletePermanent": "Supprimer définitivement" + }, + "dialogCreatePageNameHint": "Nom de la page", + "questionBubble": { + "whatsNew": "Nouveautés", + "help": "Aide et Support", + "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" + } + }, + "menuAppHeader": { + "addPageTooltip": "Ajoutez rapidement une page à l'intérieur", + "defaultNewPageName": "Sans-titre", + "renameDialog": "Renommer" + }, + "toolbar": { + "undo": "Annuler", + "redo": "Rétablir", + "bold": "Gras", + "italic": "Italique", + "underline": "Souligner", + "strike": "Barré", + "numList": "Liste numérotée", + "bulletList": "Liste à puces", + "checkList": "To-Do List", + "inlineCode": "Code", + "quote": "Bloc de citation", + "header": "En-tête", + "highlight": "Surligner" + }, + "tooltip": { + "lightMode": "Passer en mode clair", + "darkMode": "Passer en mode sombre", + "openAsPage": "Ouvrir en tant que page", + "addNewRow": "Ajouter une ligne", + "openMenu": "Cliquer pour ouvrir le menu" + }, + "sideBar": { + "closeSidebar": "Fermer le menu latéral", + "openSidebar": "Ouvrir le menu latéral" + }, + "notifications": { + "export": { + "markdown": "Note exportée en Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "Contacts", + "whatsHappening": "Que se passe-t-il cette semaine ?", + "addContact": "Ajouter un contact", + "editContact": "Modifier le contact" + }, + "button": { + "OK": "OK", + "Cancel": "Annuler", + "signIn": "Se connecter", + "signOut": "Se déconnecter", + "complete": "Achevé", + "save": "Enregistrer" + }, + "label": { + "welcome": "Bienvenue !", + "firstName": "Prénom", + "middleName": "Deuxième prénom", + "lastName": "Nom", + "stepX": "Étape {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Impossible de se connecter à votre compte.", + "failedMsg": "Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur." + }, + "google": { + "title": "CONNEXION VIA GOOGLE", + "instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur web.", + "instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:", + "instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus:", + "instruction4": "Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription:" + } + }, + "settings": { + "title": "Paramètres", + "menu": { + "appearance": "Apparence", + "language": "Langue", + "user": "Utilisateur", + "open": "Ouvrir les paramètres" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Mode clair", + "dark": "Mode sombre", + "system": "Adapt to System" + } + } + }, + "grid": { + "settings": { + "filter": "Filtrer", + "sortBy": "Filtrer par", + "Properties": "Propriétés", + "group": "Groupe" + }, + "field": { + "hide": "Cacher", + "insertLeft": "Insérer à gauche", + "insertRight": "Insérer à droite", + "duplicate": "Dupliquer", + "delete": "Supprimer", + "textFieldName": "Texte", + "checkboxFieldName": "Case à cocher", + "dateFieldName": "Date", + "numberFieldName": "Nombre", + "singleSelectFieldName": "Sélectionner", + "multiSelectFieldName": "Multisélection", + "urlFieldName": "URL", + "numberFormat": "Format du nombre", + "dateFormat": "Format de la date", + "includeTime": "Inclure l'heure", + "dateFormatFriendly": "Mois Jour, Année", + "dateFormatISO": "Année-Mois-Jour", + "dateFormatLocal": "Mois/Jour/Année", + "dateFormatUS": "Année/Mois/Jour", + "timeFormat": "Format du temps", + "invalidTimeFormat": "Format invalide", + "timeFormatTwelveHour": "12 heures", + "timeFormatTwentyFourHour": "24 heures", + "addSelectOption": "Ajouter une option", + "optionTitle": "Options", + "addOption": "Ajouter une option", + "editProperty": "Modifier la propriété", + "newProperty": "Nouvelle colonne", + "deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?" + }, + "row": { + "duplicate": "Dupliquer", + "delete": "Supprimer", + "textPlaceholder": "Vide", + "copyProperty": "Copie de la propriété dans le presse-papiers", + "count": "Nombre", + "newRow": "Nouvelle ligne" + }, + "selectOption": { + "create": "Créer", + "purpleColor": "Violet", + "pinkColor": "Rose", + "lightPinkColor": "Rose clair", + "orangeColor": "Orange", + "yellowColor": "Jaune", + "limeColor": "Citron vert", + "greenColor": "Vert", + "aquaColor": "Aqua", + "blueColor": "Bleu", + "deleteTag": "Supprimer l'étiquette", + "colorPanelTitle": "Couleurs", + "panelTitle": "Sélectionnez une option ou créez-en une", + "searchOption": "Rechercher une option" + }, + "menuName": "Grille" + }, + "document": { + "menuName": "Document", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + } + }, + "board": { + "column": { + "create_new_card": "Nouveau" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/hu-HU.json b/frontend/appflowy_flutter/assets/translations/hu-HU.json new file mode 100644 index 0000000000..17034d3c7b --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/hu-HU.json @@ -0,0 +1,153 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Én", + "welcomeText": "Üdvözöl az @:appName", + "githubStarText": "GitHub csillagozás", + "subscribeNewsletterText": "Iratkozz fel a hírlevelünkre", + "letsGoButtonText": "Vágjunk bele", + "title": "Cím", + "signUp": { + "buttonText": "Regisztráció", + "title": "Regisztrálj az @:appName -ra", + "getStartedText": "Kezdés", + "emptyPasswordError": "A jelszó nem lehet üres", + "repeatPasswordEmptyError": "A jelszó megerősítése nem lehet üres", + "unmatchedPasswordError": "A jelszavak nem egyeznek", + "alreadyHaveAnAccount": "Rendelkezel már fiókkal?", + "emailHint": "Email", + "passwordHint": "Jelszó", + "repeatPasswordHint": "Jelszó megerősítése" + }, + "signIn": { + "loginTitle": "Bejelentkezés az @:appName -ba", + "loginButtonText": "Belépés", + "buttonText": "Bejelentkezés", + "forgotPassword": "Elfelejtett jelszó?", + "emailHint": "Email", + "passwordHint": "Jelszó", + "dontHaveAnAccount": "Még nincs fiókod?", + "repeatPasswordEmptyError": "A jelszó megerősítése nem lehet üres", + "unmatchedPasswordError": "A jelszavak nem egyeznek" + }, + "workspace": { + "create": "Új munkaterület létrehozása", + "hint": "munkaterület", + "notFoundError": "munkaterület nem található" + }, + "shareAction": { + "buttonText": "Megosztás", + "workInProgress": "Hamarosan érkezik...", + "markdown": "Markdown", + "copyLink": "Link másolása" + }, + "disclosureAction": { + "rename": "Átnevezés", + "delete": "Törlés", + "duplicate": "Duplikálás" + }, + "blankPageTitle": "Üres oldal", + "newPageText": "Új oldal", + "trash": { + "text": "Kuka", + "restoreAll": "Összes visszaállítása", + "deleteAll": "Összes törlése", + "pageHeader": { + "fileName": "Fájlnév", + "lastModified": "Utoljára módosítva", + "created": "Létrehozva" + } + }, + "deletePagePrompt": { + "text": "Ez az oldal a kukában van", + "restore": "Oldal visszaállítása", + "deletePermanent": "Végleges törlés" + }, + "dialogCreatePageNameHint": "Oldalnév", + "questionBubble": { + "whatsNew": "Újdonságok", + "help": "Segítség & Támogatás", + "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" + } + }, + "menuAppHeader": { + "addPageTooltip": "Belső oldal hozzáadása", + "defaultNewPageName": "Névtelen", + "renameDialog": "Átnevezés" + }, + "toolbar": { + "undo": "Vissza", + "redo": "Előre", + "bold": "Félkövér", + "italic": "Dőlt", + "underline": "Aláhúzott", + "strike": "Áthúzott", + "numList": "Számozott lista", + "bulletList": "Felsorolás", + "checkList": "Ellenőrző lista", + "inlineCode": "Inline kód", + "quote": "Idézet", + "header": "Címsor", + "highlight": "Kiemelés" + }, + "tooltip": { + "lightMode": "Világos mód", + "darkMode": "Éjjeli mód" + }, + "contactsPage": { + "title": "Kontaktok", + "whatsHappening": "Heti újdonságok", + "addContact": "Új Kontakt", + "editContact": "Kontakt Szerkesztése" + }, + "button": { + "OK": "OK", + "Cancel": "Mégse", + "signIn": "Bejelentkezés", + "signOut": "Kijelentkezés", + "complete": "Kész", + "save": "Mentés" + }, + "label": { + "welcome": "Üdvözlünk!", + "firstName": "Keresztnév", + "middleName": "Középső név", + "lastName": "Vezetéknév", + "stepX": "{X}. lépés" + }, + "oAuth": { + "err": { + "failedTitle": "Sikertelen bejelentkezés.", + "failedMsg": "Kérjük győződj meg róla, hogy elvégezted a bejelentkezési folyamatot a böngésződben" + }, + "google": { + "title": "Bejelentkezés Google-al", + "instruction1": "Ahhoz, hogy hozzáférj a Google Kontaktjaidhoz, kérjük hatalmazd fel ezt az alkalmazást a böngésződben.", + "instruction2": "Másold ezt a kódot a vágólapra az ikonra kattintással vagy a szöveg kijelölésével:", + "instruction3": "Nyisd meg ezt a linket a böngésződben, és írjd be a fenti kódot:", + "instruction4": "Nyomd meg az alábbi gombot, ha elvégezted a registrációt:" + } + }, + "settings": { + "title": "Beállítások", + "menu": { + "appearance": "Megjelenés", + "language": "Nyelv", + "open": "Beállítások megnyitása" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Világos mód", + "dark": "Éjjeli mód", + "system": "Adapt to System" + } + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/id-ID.json b/frontend/appflowy_flutter/assets/translations/id-ID.json new file mode 100644 index 0000000000..2e6b348b2c --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/id-ID.json @@ -0,0 +1,226 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Saya", + "welcomeText": "Selamat datang di @:appName", + "githubStarText": "Bintangi GitHub", + "subscribeNewsletterText": "Berlangganan buletin", + "letsGoButtonText": "Ayo", + "title": "Judul", + "signUp": { + "buttonText": "Daftar", + "title": "Daftar ke @:appName", + "getStartedText": "Mulai", + "emptyPasswordError": "Sandi tidak boleh kosong", + "repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong", + "unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi", + "alreadyHaveAnAccount": "Sudah punya akun?", + "emailHint": "Email", + "passwordHint": "Sandi", + "repeatPasswordHint": "Sandi ulang" + }, + "signIn": { + "loginTitle": "Masuk ke @:appName", + "loginButtonText": "Masuk", + "buttonText": "Masuk", + "forgotPassword": "Lupa Sandi?", + "emailHint": "Email", + "passwordHint": "Sandi", + "dontHaveAnAccount": "Belum punya akun?", + "repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong", + "unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi" + }, + "workspace": { + "create": "Buat workspace", + "hint": "workspace", + "notFoundError": "Workspace tidak ditemukan" + }, + "shareAction": { + "buttonText": "Bagikan", + "workInProgress": "Segera", + "markdown": "Markdown", + "copyLink": "Salin tautan" + }, + "disclosureAction": { + "rename": "Ganti nama", + "delete": "Hapus", + "duplicate": "Duplikat" + }, + "blankPageTitle": "Halaman kosong", + "newPageText": "Halaman baru", + "trash": { + "text": "Sampah", + "restoreAll": "Pulihkan Semua", + "deleteAll": "Hapus semua", + "pageHeader": { + "fileName": "Nama file", + "lastModified": "Terakhir diubah", + "created": "Dibuat" + } + }, + "deletePagePrompt": { + "text": "Halaman ini di tempat sampah", + "restore": "Pulihkan halaman", + "deletePermanent": "Hapus secara permanen" + }, + "dialogCreatePageNameHint": "Nama halaman", + "questionBubble": { + "whatsNew": "Apa yang baru?", + "help": "Bantuan & Dukungan", + "debug": { + "name": "Info debug", + "success": "Info debug disalin ke papan klip!", + "fail": "Tidak dapat menyalin info debug ke papan klip" + } + }, + "menuAppHeader": { + "addPageTooltip": "Menambahkan halaman di dalam dengan cepat", + "defaultNewPageName": "Tanpa Judul", + "renameDialog": "Ganti nama" + }, + "toolbar": { + "undo": "Undo", + "redo": "Redo", + "bold": "Tebal", + "italic": "Miring", + "underline": "Garis bawah", + "strike": "Dicoret", + "numList": "Daftar bernomor", + "bulletList": "Daftar berpoin", + "checkList": "Daftar periksa", + "inlineCode": "Kode sebaris", + "quote": "Blok kutipan", + "header": "Tajuk", + "highlight": "Sorotan" + }, + "tooltip": { + "lightMode": "Ganti mode terang", + "darkMode": "Ganti mode gelap" + }, + "notifications": { + "export": { + "markdown": "Mengekspor Catatan ke Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "Kontak", + "whatsHappening": "Apa yang terjadi minggu ini?", + "addContact": "Tambahkan Kontak", + "editContact": "Ubah Kontak" + }, + "button": { + "OK": "Ya", + "Cancel": "Batal", + "signIn": "Masuk", + "signOut": "Keluar", + "complete": "Selesai", + "save": "Simpan" + }, + "label": { + "welcome": "Selamat datang!", + "firstName": "Nama Depan", + "middleName": "Nama Tengah", + "lastName": "Nama Akhir", + "stepX": "Langkah {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Tidak dapat terhubung ke akun anda", + "failedMsg": "Mohon pastikan anda menyelesaikan proses pendaftaran pada browser anda." + }, + "google": { + "title": "MASUK GOOGLE", + "instruction1": "Untuk mengimpor kontak Google Contacts anda, anda harus mengizinkan aplikasi ini menggunakan browser web anda.", + "instruction2": "Salin kode ini ke papan klip anda dengan cara mengklik ikon atau memilih teks:", + "instruction3": "Arahkan ke tautan berikut di browser web Anda, dan masukkan kode di atas:", + "instruction4": "Tekan tombol di bawah ini setelah Anda menyelesaikan pendaftaran:" + } + }, + "settings": { + "title": "Pengaturan", + "menu": { + "appearance": "Tampilan", + "language": "Bahasa", + "user": "Pengguna", + "open": "Buka Pengaturan" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Mode Terang", + "dark": "Mode Gelap", + "system": "Adapt to System" + } + } + }, + "grid": { + "settings": { + "filter": "Filter", + "sortBy": "Sortir dengan", + "Properties": "Properti" + }, + "field": { + "hide": "Sembunyikan", + "insertLeft": "Sisipkan Kiri", + "insertRight": "Sisipkan Kanan", + "duplicate": "Duplikasi", + "delete": "Hapus", + "textFieldName": "Teks", + "checkboxFieldName": "Kotak Centang", + "dateFieldName": "Tanggal", + "numberFieldName": "Angka", + "singleSelectFieldName": "seleksi", + "multiSelectFieldName": "Multi seleksi", + "urlFieldName": "URL", + "numberFormat": "Format angka", + "dateFormat": "Format tanggal", + "includeTime": "Sertakan waktu", + "dateFormatFriendly": "Bulan Hari, Tahun", + "dateFormatISO": "Tahun-Bulan-Hari", + "dateFormatLocal": "Bulan/Hari/Tahun", + "dateFormatUS": "Tahun/Bulan/Hari", + "timeFormat": "Format waktu", + "invalidTimeFormat": "Format yang tidak valid", + "timeFormatTwelveHour": "12 jam", + "timeFormatTwentyFourHour": "24 jam", + "addSelectOption": "Tambahkan opsi", + "optionTitle": "Opsi", + "addOption": "Tambahkan opsi", + "editProperty": "Ubah properti" + }, + "row": { + "duplicate": "Duplikasi", + "delete": "Hapus", + "textPlaceholder": "Kosong", + "copyProperty": "Salin properti ke papan klip" + }, + "selectOption": { + "create": "Buat", + "purpleColor": "Ungu", + "pinkColor": "Merah Jambu", + "lightPinkColor": "Merah Jambu Muda", + "orangeColor": "Oranye", + "yellowColor": "Kuning", + "limeColor": "Limau", + "greenColor": "Hijau", + "aquaColor": "Air", + "blueColor": "Biru", + "deleteTag": "Hapus tag", + "colorPanelTitle": "Warna", + "panelTitle": "Pilih opsi atau buat baru", + "searchOption": "Cari opsi" + }, + "menuName": "Grid" + }, + "document": { + "menuName": "Dokter", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/it-IT.json b/frontend/appflowy_flutter/assets/translations/it-IT.json new file mode 100644 index 0000000000..cf2e19d9e6 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/it-IT.json @@ -0,0 +1,159 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Me", + "welcomeText": "Benvenuto in @:appName", + "githubStarText": "Vota su GitHub", + "subscribeNewsletterText": "Sottoscrivi la Newsletter", + "letsGoButtonText": "Andiamo", + "title": "Titolo", + "signUp": { + "buttonText": "Registrati", + "title": "Registrati per @:appName", + "getStartedText": "Iniziamo", + "emptyPasswordError": "La password non può essere vuota", + "repeatPasswordEmptyError": "La password ripetuta non può essere vuota", + "unmatchedPasswordError": "La password ripetuta non è uguale alla password", + "alreadyHaveAnAccount": "Hai già un account?", + "emailHint": "Email", + "passwordHint": "Password", + "repeatPasswordHint": "Ripeti password" + }, + "signIn": { + "loginTitle": "Accedi a @:appName", + "loginButtonText": "Login", + "buttonText": "Accedi", + "forgotPassword": "Password Dimentica?", + "emailHint": "Email", + "passwordHint": "Password", + "dontHaveAnAccount": "Non hai un account?", + "repeatPasswordEmptyError": "La password ripetuta non può essere vuota", + "unmatchedPasswordError": "La password ripetuta non è uguale alla password" + }, + "workspace": { + "create": "Crea spazio di lavoro", + "hint": "spazio di lavoro", + "notFoundError": "Spazio di lavoro non trovato" + }, + "shareAction": { + "buttonText": "Condividi", + "workInProgress": "Prossimamente", + "markdown": "Markdown", + "copyLink": "Copia Link" + }, + "disclosureAction": { + "rename": "Rinomina", + "delete": "Cancella", + "duplicate": "Duplica" + }, + "blankPageTitle": "Pagina vuota", + "newPageText": "Nuova pagina", + "trash": { + "text": "Cestino", + "restoreAll": "Ripristina Tutto", + "deleteAll": "Elimina Tutto", + "pageHeader": { + "fileName": "Nome file", + "lastModified": "Ultima Modifica", + "created": "Creato" + } + }, + "deletePagePrompt": { + "text": "Questa pagina è nel Cestino", + "restore": "Ripristina pagina", + "deletePermanent": "Elimina definitivamente" + }, + "dialogCreatePageNameHint": "Nome pagina", + "questionBubble": { + "whatsNew": "Cosa c'è di nuovo?", + "help": "Aiuto & Supporto", + "debug": { + "name": "Informazioni di debug", + "success": "Informazioni di debug copiate negli appunti!", + "fail": "Impossibile copiare le informazioni di debug negli appunti" + } + }, + "menuAppHeader": { + "addPageTooltip": "Aggiungi velocemente una pagina all'interno", + "defaultNewPageName": "Senza titolo", + "renameDialog": "Rinomina" + }, + "toolbar": { + "undo": "Undo", + "redo": "Redo", + "bold": "Grassetto", + "italic": "Italico", + "underline": "Sottolineato", + "strike": "Barrato", + "numList": "Lista numerata", + "bulletList": "Lista a punti", + "checkList": "Lista Controllo", + "inlineCode": "Codice in linea", + "quote": "Cita Blocco", + "header": "intestazione", + "highlight": "Evidenziare" + }, + "tooltip": { + "lightMode": "Passa alla modalità Chiara", + "darkMode": "Passa alla modalità Scura" + }, + "contactsPage": { + "title": "Contatti", + "whatsHappening": "Cosa accadrà la prossima settimana?", + "addContact": "Aggiungi Contatti", + "editContact": "Modifica Contatti" + }, + "button": { + "OK": "OK", + "Cancel": "Annulla", + "signIn": "Accedi", + "signOut": "Esci", + "complete": "Completa", + "save": "Salva" + }, + "label": { + "welcome": "Benvenuto!", + "firstName": "Name", + "middleName": "Secondo Name", + "lastName": "Cognome", + "stepX": "Passo {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Impossibile collegarsi al tuo account.", + "failedMsg": "Si prega di verificare di aver completato il processo di iscrizione nel tuo browser." + }, + "google": { + "title": "GOOGLE SIGN-IN", + "instruction1": "Al fine di importare i tuoi Contatti Google è necessario autorizzare questa applicazione ad utilizzare il tuo beowser web.", + "instruction2": "Copia questo codice negli appunti premendo l'icona o selezionando il testo:", + "instruction3": "Naviga sul seguente link con il tuo browser web e inserisci il codice seguente:", + "instruction4": "Premi il bottone qui sotto quando hai completato l'iscrizione:" + } + }, + "settings": { + "title": "Impostazioni", + "menu": { + "appearance": "Aspetto", + "language": "Lingua", + "open": "aprire le impostazioni" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Modalità Chiara", + "dark": "Modalità Scura", + "system": "Adapt to System" + } + } + }, + "grid": { + "menuName": "Griglia" + }, + "document": { + "menuName": "Documento" + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/ja-JP.json b/frontend/appflowy_flutter/assets/translations/ja-JP.json new file mode 100644 index 0000000000..7b46750779 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/ja-JP.json @@ -0,0 +1,207 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "ユーザー", + "welcomeText": "Welcome to @:appName", + "githubStarText": "Star on GitHub", + "subscribeNewsletterText": "新着情報を受け取る", + "letsGoButtonText": "Let's Go", + "title": "タイトル", + "signUp": { + "buttonText": "新規登録", + "title": "@:appNameに新規登録", + "getStartedText": "はじめる", + "emptyPasswordError": "パスワードを空にはできません", + "repeatPasswordEmptyError": "パスワード(確認用)を空にはできません", + "unmatchedPasswordError": "パスワード(確認用)が一致しません", + "alreadyHaveAnAccount": "すでにアカウントを登録済ですか?", + "emailHint": "メールアドレス", + "passwordHint": "パスワード", + "repeatPasswordHint": "パスワード(確認用)" + }, + "signIn": { + "loginTitle": "@:appName にログイン", + "loginButtonText": "ログイン", + "buttonText": "サインイン", + "forgotPassword": "パスワードをお忘れですか?", + "emailHint": "メールアドレス", + "passwordHint": "パスワード", + "dontHaveAnAccount": "まだアカウントをお持ちではないですか?", + "repeatPasswordEmptyError": "パスワード(確認用)を空にはできません", + "unmatchedPasswordError": "パスワード(確認用)が一致しません" + }, + "workspace": { + "create": "ワークスペースを作成する", + "hint": "ワークスペース", + "notFoundError": "ワークスペースがみつかりません" + }, + "shareAction": { + "buttonText": "共有する", + "workInProgress": "Coming soon", + "markdown": "Markdown", + "copyLink": "Copy Link" + }, + "disclosureAction": { + "rename": "名前を変更", + "delete": "削除", + "duplicate": "コピーを作成" + }, + "blankPageTitle": "空のページ", + "newPageText": "新しいページ", + "trash": { + "text": "ごみ箱", + "restoreAll": "全て復元", + "deleteAll": "全て削除", + "pageHeader": { + "fileName": "ファイル名", + "lastModified": "最終更新日時", + "created": "作成日時" + } + }, + "deletePagePrompt": { + "text": "このページはごみ箱にあります", + "restore": "ページを元に戻す", + "deletePermanent": "削除する" + }, + "dialogCreatePageNameHint": "ページ名", + "questionBubble": { + "whatsNew": "What's new?", + "help": "ヘルプとサポート", + "debug": { + "name": "デバッグ情報", + "success": "デバッグ情報をクリップボードにコピーしました!", + "fail": "デバッグ情報をクリップボードにコピーできませんでした" + } + }, + "menuAppHeader": { + "addPageTooltip": "内部ページを追加", + "defaultNewPageName": "Untitled", + "renameDialog": "名前を変更" + }, + "toolbar": { + "undo": "元に戻す", + "redo": "やり直し", + "bold": "太字", + "italic": "斜体", + "underline": "下線", + "strike": "取り消し線", + "numList": "番号付きリスト", + "bulletList": "箇条書き", + "checkList": "チェックボックス", + "inlineCode": "インラインコード", + "quote": "引用文", + "header": "見出し", + "highlight": "文字の背景色" + }, + "tooltip": { + "lightMode": "ライトモードに切り替える", + "darkMode": "ダークモードに切り替える" + }, + "contactsPage": { + "title": "連絡先", + "whatsHappening": "今週はどんなことがありましたか?", + "addContact": "連絡先を追加する", + "editContact": "連絡先を編集する" + }, + "button": { + "OK": "OK", + "Cancel": "キャンセル", + "signIn": "サインイン", + "signOut": "サインアウト", + "complete": "完了", + "save": "保存" + }, + "label": { + "welcome": "ようこそ!", + "firstName": "名", + "middleName": "ミドルネーム", + "lastName": "姓", + "stepX": "Step {X}" + }, + "oAuth": { + "err": { + "failedTitle": "アカウントに接続できません", + "failedMsg": "サインインが完了したことをブラウザーで確認してください" + }, + "google": { + "title": "GOOGLEでサインイン", + "instruction1": "GOOGLEでのサインインを有効にするためには、Webブラウザーを使ってこのアプリケーションを認証する必要があります。", + "instruction2": "アイコンをクリックするか、以下のテキストを選択して、このコードをクリップボードにコピーします。", + "instruction3": "以下のリンク先をブラウザーで開いて、次のコードを入力します。", + "instruction4": "登録が完了したら以下のボタンを押してください。" + } + }, + "settings": { + "title": "設定", + "menu": { + "appearance": "外観", + "language": "言語", + "open": "設定" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "ライトモード", + "dark": "ダークモード", + "system": "Adapt to System" + } + } + }, + "grid": { + "settings": { + "filter": "絞り込み", + "sortBy": "並び替え", + "Properties": "プロパティ" + }, + "field": { + "hide": "隠す", + "insertLeft": "左に挿入", + "insertRight": "右に挿入", + "duplicate": "コピーを作成", + "delete": "削除", + "textFieldName": "テキスト", + "checkboxFieldName": "チェックボックス", + "dateFieldName": "日付", + "numberFieldName": "数値", + "singleSelectFieldName": "単一選択", + "multiSelectFieldName": "複数選択", + "numberFormat": "数値書式", + "dateFormat": "日付書式", + "includeTime": "時刻を含める", + "dateFormatFriendly": "月 日, 年", + "dateFormatISO": "年-月-日", + "dateFormatLocal": "月/日/年", + "dateFormatUS": "年/月/日", + "timeFormat": "時刻書式", + "timeFormatTwelveHour": "12 時間表記", + "timeFormatTwentyFourHour": "24 時間表記", + "addSelectOption": "選択候補追加", + "optionTitle": "選択候補", + "addOption": "選択候補追加", + "editProperty": "プロパティの編集" + }, + "row": { + "duplicate": "コピーを作成", + "delete": "削除", + "textPlaceholder": "空白" + }, + "selectOption": { + "purpleColor": "紫", + "pinkColor": "ピンク", + "lightPinkColor": "ライトピンク", + "orangeColor": "オレンジ", + "yellowColor": "黄色", + "limeColor": "ライム", + "greenColor": "緑", + "aquaColor": "水色", + "blueColor": "青", + "deleteTag": "選択候補を削除", + "colorPanelTitle": "色", + "panelTitle": "選択候補を検索 または 作成する", + "searchOption": "選択候補を検索" + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/ko-KR.json b/frontend/appflowy_flutter/assets/translations/ko-KR.json new file mode 100644 index 0000000000..1ed314b9d6 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/ko-KR.json @@ -0,0 +1,235 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Me", + "welcomeText": "@:appName 에 오신것을 환영합니다", + "githubStarText": "Star on GitHub", + "subscribeNewsletterText": "뉴스레터 구독", + "letsGoButtonText": "Let's Go", + "title": "제목", + "signUp": { + "buttonText": "회원가입", + "title": "@:appName 에 회원가입", + "getStartedText": "시작하기", + "emptyPasswordError": "비밀번호는 공백일 수 없습니다", + "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", + "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다", + "alreadyHaveAnAccount": "이미 계정이 있으신가요?", + "emailHint": "이메일", + "passwordHint": "비밀번호", + "repeatPasswordHint": "비밀번호 재입력" + }, + "signIn": { + "loginTitle": "@:appName 에 로그인", + "loginButtonText": "로그인", + "buttonText": "로그인", + "forgotPassword": "비밀번호를 잊으셨나요?", + "emailHint": "이메일", + "passwordHint": "비밀번호", + "dontHaveAnAccount": "계정이 없으신가요?", + "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", + "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다" + }, + "workspace": { + "create": "워크스페이스 생성", + "hint": "워크스페이스", + "notFoundError": "워크스페이스를 찾을 수 없습니다" + }, + "shareAction": { + "buttonText": "공유", + "workInProgress": "Coming soon", + "markdown": "마크다운", + "copyLink": "링크 복사" + }, + "disclosureAction": { + "rename": "이름변경", + "delete": "삭제", + "duplicate": "복제" + }, + "blankPageTitle": "빈 페이지", + "newPageText": "새로운 페이지", + "trash": { + "text": "휴지통", + "restoreAll": "모두 복구", + "deleteAll": "모두 삭제", + "pageHeader": { + "fileName": "파일 이름", + "lastModified": "수정날짜", + "created": "생성날짜" + } + }, + "deletePagePrompt": { + "text": "현재 페이지는 휴지통에 있습니다", + "restore": "페이지 복구", + "deletePermanent": "영구 삭제" + }, + "dialogCreatePageNameHint": "페이지 이름", + "questionBubble": { + "whatsNew": "새로운 소식", + "help": "도움 및 지원", + "debug": { + "name": "디버그 정보", + "success": "디버그 정보를 클립보드로 복사했습니다.", + "fail": "디버그 정보를 클립보드로 복사할 수 없습니다." + } + }, + "menuAppHeader": { + "addPageTooltip": "하위에 페이지 추가", + "defaultNewPageName": "제목없음", + "renameDialog": "이름변경" + }, + "toolbar": { + "undo": "실행취소", + "redo": "재실행", + "bold": "굵게", + "italic": "기울임꼴", + "underline": "밑줄", + "strike": "취소선", + "numList": "번호 매기기 목록", + "bulletList": "글머리 기호 목록", + "checkList": "작업 목록", + "inlineCode": "인라인 코드", + "quote": "인용구 블록", + "header": "헤더", + "highlight": "하이라이트" + }, + "tooltip": { + "lightMode": "라이트 모드로 변경", + "darkMode": "다크 모드로 변경", + "openAsPage": "페이지로 열기", + "addNewRow": "열 추가", + "openMenu": "메뉴를 여시려면 클릭하세요" + }, + "sideBar": { + "closeSidebar": "사이드바 닫기", + "openSidebar": "사이드바 열기" + }, + "notifications": { + "export": { + "markdown": "마크다운으로 노트를 내보냄", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "연락처", + "whatsHappening": "이번주에는 무슨 일이 있나요?", + "addContact": "연락처 추가", + "editContact": "연락처 편집" + }, + "button": { + "OK": "확인", + "Cancel": "취소", + "signIn": "로그인", + "signOut": "로그아웃", + "complete": "완료", + "save": "저장" + }, + "label": { + "welcome": "환영합니다!", + "firstName": "이름", + "middleName": "중간 이름", + "lastName": "성", + "stepX": "{X} 단계" + }, + "oAuth": { + "err": { + "failedTitle": "계정에 연결을 할 수 없습니다.", + "failedMsg": "브라우저에서 회원가입이 완료되었는지 확인해주세요." + }, + "google": { + "title": "GOOGLE SIGN-IN", + "instruction1": "구글 연락처를 가져오기 위해서 웹브라우저로 앱을 승인 해야 합니다.", + "instruction2": "아이콘을 클릭 또는 텍스트를 선택해서 이 코드를 클립보드로 복사하세요:", + "instruction3": "웹브라우저로 다음 링크로 가셔서 위 코드를 입력해주세요:", + "instruction4": "가입 완료 후 아래 버튼을 눌러주세요:" + } + }, + "settings": { + "title": "설정", + "menu": { + "appearance": "화면", + "language": "언어", + "user": "사용자", + "open": "설정 열기" + }, + "appearance": { + "lightLabel": "라이트 모드", + "darkLabel": "다크 모드" + } + }, + "grid": { + "settings": { + "filter": "필터", + "sortBy": "정렬 기준", + "Properties": "속성", + "group": "그룹" + }, + "field": { + "hide": "숨기기", + "insertLeft": "왼쪽 삽입", + "insertRight": "오른쪽 삽입", + "duplicate": "복제", + "delete": "삭제", + "textFieldName": "텍스트", + "checkboxFieldName": "체크박스", + "dateFieldName": "날짜", + "numberFieldName": "숫자", + "singleSelectFieldName": "선택", + "multiSelectFieldName": "다중선택", + "urlFieldName": "링크", + "numberFormat": "숫자 형식", + "dateFormat": "날짜 형식", + "includeTime": "시간 표시", + "dateFormatFriendly": "월 일, 년", + "dateFormatISO": "년-월-일", + "dateFormatLocal": "월/일/년", + "dateFormatUS": "년/월/일", + "timeFormat": "시간 형식", + "invalidTimeFormat": "잘못된 형식", + "timeFormatTwelveHour": "12 시간", + "timeFormatTwentyFourHour": "24 시간", + "addSelectOption": "옵션 추가", + "optionTitle": "옵션", + "addOption": "옵션 추가", + "editProperty": "속성 편집", + "newProperty": "열 추가", + "deleteFieldPromptMessage": "해당 속성을 삭제 하시겠습니까?" + }, + "row": { + "duplicate": "복제", + "delete": "삭제", + "textPlaceholder": "비어있음", + "copyProperty": "속성이 클립보드로 복사됨", + "count": "개수", + "newRow": "행 추가" + }, + "selectOption": { + "create": "생성", + "purpleColor": "보라색", + "pinkColor": "핑크색", + "lightPinkColor": "연한 핑크색", + "orangeColor": "오렌지색", + "yellowColor": "노랑색", + "limeColor": "라임색", + "greenColor": "초록색", + "aquaColor": "아쿠아색", + "blueColor": "파랑색", + "deleteTag": "태그 삭제", + "colorPanelTitle": "색상", + "panelTitle": "옵션 선택 또는 생성", + "searchOption": "옵션 검색" + }, + "menuName": "그리드" + }, + "document": { + "menuName": "도큐먼트", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + } + }, + "board": { + "column": { + "create_new_card": "추가" + } + } +} \ No newline at end of file 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/assets/translations/pl-PL.json b/frontend/appflowy_flutter/assets/translations/pl-PL.json new file mode 100644 index 0000000000..03e7cfd284 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/pl-PL.json @@ -0,0 +1,153 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Ja", + "welcomeText": "Witaj w @:appName", + "githubStarText": "Gwiazdka na GitHub-ie", + "subscribeNewsletterText": "Zapisz się do naszego Newslettera", + "letsGoButtonText": "Start!", + "title": "Tytuł", + "signUp": { + "buttonText": "Zarejestruj", + "title": "Zarejestruj się w @:appName", + "getStartedText": "Zaczynamy", + "emptyPasswordError": "Hasło nie moze być puste", + "repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste", + "unmatchedPasswordError": "Hasła nie są takie same", + "alreadyHaveAnAccount": "Masz juz konto?", + "emailHint": "Email", + "passwordHint": "Hasło", + "repeatPasswordHint": "Powtórz hasło" + }, + "signIn": { + "loginTitle": "Zaloguj do @:appName", + "loginButtonText": "Logowanie", + "buttonText": "Zaloguj", + "forgotPassword": "Zapomniałem hasła?", + "emailHint": "Email", + "passwordHint": "Password", + "dontHaveAnAccount": "Nie masz konta?", + "repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste", + "unmatchedPasswordError": "Hasła nie są takie same" + }, + "workspace": { + "create": "Utwórz przestrzeń", + "hint": "przestrzeń robocza", + "notFoundError": "Przestrzeni nie znaleziono" + }, + "shareAction": { + "buttonText": "Udostępnij", + "workInProgress": "Wkrótce", + "markdown": "Markdown", + "copyLink": "Skopiuj link" + }, + "disclosureAction": { + "rename": "Zmień nazwę", + "delete": "Usuń", + "duplicate": "Duplikuj" + }, + "blankPageTitle": "Pusta strona", + "newPageText": "Nowa strona", + "trash": { + "text": "Kosz", + "restoreAll": "Przywróć Wszystko", + "deleteAll": "Usuń Wszystko", + "pageHeader": { + "fileName": "Nazwa Pliku", + "lastModified": "Ostatnio Zmodyfikowano", + "created": "Utworzono" + } + }, + "deletePagePrompt": { + "text": "Ta strona jest w Koszu", + "restore": "Przywróć strone", + "deletePermanent": "Usuń bezpowrotnie" + }, + "dialogCreatePageNameHint": "Nazwa Strony", + "questionBubble": { + "whatsNew": "What's new?", + "help": "Pomoc & Wsparcie", + "debug": { + "name": "Informacje Debugowania", + "success": "Skopiowano informacje debugowania do schowka!", + "fail": "Nie mozna skopiować informacji debugowania do schowka" + } + }, + "menuAppHeader": { + "addPageTooltip": "Szybko dodaj stronę do środka", + "defaultNewPageName": "Brak tytułu", + "renameDialog": "Zmień nazwę" + }, + "toolbar": { + "undo": "Cofnij", + "redo": "Powtórz", + "bold": "Pogrubiony", + "italic": "Kursywa", + "underline": "Podkreśl", + "strike": "Przekreśl", + "numList": "Lista Numerowana", + "bulletList": "Lista Punktowana", + "checkList": "Lista Kontrolna", + "inlineCode": "Kod Wbudowany", + "quote": "Blok cytat", + "header": "Nagłówek", + "highlight": "Podświetl" + }, + "tooltip": { + "lightMode": "Przełącz w Tryb Jasny", + "darkMode": "Przełącz w Tryb Ciemny" + }, + "contactsPage": { + "title": "Kontakty", + "whatsHappening": "Co się dzieje w tym tygodniu?", + "addContact": "Dodaj Kontakt", + "editContact": "Edytuj Kontakt" + }, + "button": { + "OK": "OK", + "Cancel": "Anuluj", + "signIn": "Zaloguj", + "signOut": "Wyloguj", + "complete": "Zakończono", + "save": "Zapisz" + }, + "label": { + "welcome": "Witaj!", + "firstName": "Imię Pierwsze", + "middleName": "Imię Drugie", + "lastName": "Nazwisko", + "stepX": "Krok {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Nie można połączyć się z Twoim kontem.", + "failedMsg": "Upewnij się, że zakończyłeś proces logowania w przeglądarce." + }, + "google": { + "title": "LOGOWANIE GOOGLE", + "instruction1": "Aby zaimportować Kontakty Google, musisz autoryzować tę aplikację za pomocą przeglądarki internetowej.", + "instruction2": "Skopiuj ten kod do schowka, klikając ikonę lub zaznaczając tekst:", + "instruction3": "Przejdź do następującego linku w przeglądarce internetowej i wprowadź powyższy kod:", + "instruction4": "Naciśnij poniższy przycisk po zakończeniu rejestracji:" + } + }, + "settings": { + "title": "Ustawienia", + "menu": { + "appearance": "Wygląd", + "language": "Język", + "open": "Otwórz Ustawienia" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Tryb Jasny", + "dark": "Tryb Ciemny", + "system": "Adapt to System" + } + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/pt-BR.json b/frontend/appflowy_flutter/assets/translations/pt-BR.json new file mode 100644 index 0000000000..2dd0a69ad4 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/pt-BR.json @@ -0,0 +1,373 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Eu", + "welcomeText": "Bem-vindo ao @:appName", + "githubStarText": "Dar uma estrela no Github", + "subscribeNewsletterText": "Inscreva-se para receber novidades", + "letsGoButtonText": "Vamos lá", + "title": "Título", + "signUp": { + "buttonText": "Inscreva-se", + "title": "Inscreva-se no @:appName", + "getStartedText": "Começar", + "emptyPasswordError": "Senha não pode estar em branco.", + "repeatPasswordEmptyError": "Senha não estar em branco.", + "unmatchedPasswordError": "As senhas não conferem.", + "alreadyHaveAnAccount": "Já possui uma conta?", + "emailHint": "E-mail", + "passwordHint": "Dica de senha", + "repeatPasswordHint": "Confirme a senha" + }, + "signIn": { + "loginTitle": "Conectar-se ao @:appName", + "loginButtonText": "Conectar-se", + "buttonText": "Entre", + "forgotPassword": "Esqueceu sua senha?", + "emailHint": "E-mail", + "passwordHint": "Senha", + "dontHaveAnAccount": "Não possui uma conta?", + "repeatPasswordEmptyError": "Senha não pode estar em branco.", + "unmatchedPasswordError": "As senhas não conferem." + }, + "workspace": { + "create": "Crie um espaço de trabalho", + "hint": "Espaço de trabalho", + "notFoundError": "Espaço de trabalho não encontrado" + }, + "shareAction": { + "buttonText": "Compartilhar", + "workInProgress": "Em breve", + "markdown": "Marcador", + "copyLink": "Copiar link" + }, + "moreAction": { + "small": "pequeno", + "medium": "médio", + "large": "grande", + "fontSize": "Tamanho da fonte", + "import": "Importar" + }, + "disclosureAction": { + "rename": "Renomear", + "delete": "Apagar", + "duplicate": "Duplicar" + }, + "blankPageTitle": "Página em branco", + "newPageText": "Nova página", + "trash": { + "text": "Lixeira", + "restoreAll": "Restaurar tudo", + "deleteAll": "Apagar tudo", + "pageHeader": { + "fileName": "Nome do arquivo", + "lastModified": "Última modificação", + "created": "Criado" + } + }, + "deletePagePrompt": { + "text": "Está página está na lixeira", + "restore": "Restaurar a página", + "deletePermanent": "Apagar permanentemente" + }, + "dialogCreatePageNameHint": "Nome da página", + "questionBubble": { + "shortcuts": "Atalhos", + "whatsNew": "O que há de novo?", + "help": "Ajuda e Suporte", + "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" + } + }, + "menuAppHeader": { + "addPageTooltip": "Adicionar uma nova página.", + "defaultNewPageName": "Sem título", + "renameDialog": "Renomear" + }, + "toolbar": { + "undo": "Desfazer", + "redo": "Refazer", + "bold": "Negrito", + "italic": "Itálico", + "underline": "Sublinhado", + "strike": "Tachado", + "numList": "Lista numerada", + "bulletList": "Lista com marcadores", + "checkList": "Check list", + "inlineCode": "Embutir código", + "quote": "Citação em bloco", + "header": "Cabeçalho", + "highlight": "Destacar", + "color": "Cor" + }, + "tooltip": { + "lightMode": "Mudar para o modo claro", + "darkMode": "Mudar para o modo escuro", + "openAsPage": "Abrir como uma página", + "addNewRow": "Adicionar uma nova linha", + "openMenu": "Clique para abrir o menu", + "viewDataBase": "Visualizar banco de dados", + "referencePage": "Esta {name} é uma referência" + }, + "sideBar": { + "openSidebar": "Abrir barra lateral", + "closeSidebar": "Fechar barra lateral" + }, + "notifications": { + "export": { + "markdown": "Nota exportada como um marcador", + "path": "Documentos/flowy" + } + }, + "contactsPage": { + "title": "Contatos", + "whatsHappening": "O que está acontecendo essa semana?", + "addContact": "Adicionar um contato", + "editContact": "Editar um contato" + }, + "button": { + "OK": "Ok", + "Cancel": "Cancelar", + "signIn": "Conectar", + "signOut": "Desconectar", + "complete": "Completar", + "save": "Salvar", + "generate": "Gerar", + "esc": "Sair", + "keep": "Manter", + "tryAGain": "Tentar novamente", + "discard": "Descartar", + "replace": "substituir" + }, + "label": { + "welcome": "Bem-vindo!", + "firstName": "Nome", + "middleName": "Sobrenome", + "lastName": "Último nome", + "stepX": "Passo {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Erro ao conectar a sua conta.", + "failedMsg": "Verifique se você concluiu o processo de conexão em seu navegador." + }, + "google": { + "title": "Conexão com o GOOGLE", + "instruction1": "Para importar seus Contatos do Google, você precisará autorizar este aplicativo usando seu navegador.", + "instruction2": "Copie este código para sua área de transferência clicando no ícone ou selecionando o texto:", + "instruction3": "Navegue até o link a seguir em seu navegador e digite o código acima:", + "instruction4": "Pressione o botão abaixo ao concluir a inscrição:" + } + }, + "settings": { + "title": "Configurações", + "menu": { + "appearance": "Aparência", + "language": "Idioma", + "user": "Usuário", + "files": "Arquivos", + "open": "Abrir Configurações" + }, + "appearance": { + "themeMode": { + "label": "Tema", + "light": "Modo claro", + "dark": "Modo escuro", + "system": "Adaptar-se ao sistema" + }, + "theme": "Tema" + }, + "files": { + "defaultLocation": "Onde os seus dados ficam armazenados", + "doubleTapToCopy": "Clique duas vezes para copiar o caminho", + "restoreLocation": "Restaurar para o caminho padrão do AppFlowy", + "customizeLocation": "Abrir outra pasta", + "restartApp": "Reinicie o aplicativo para que as alterações entrem em vigor.", + "exportDatabase": "Exportar banco de dados", + "selectFiles": "Escolha os arquivos que precisam ser exportados", + "createNewFolder": "Criar uma nova pasta", + "createNewFolderDesc": "Diga-nos onde pretende armazenar os seus dados ...", + "open": "Abrir", + "openFolder": "Abra uma pasta existente", + "openFolderDesc": "Gravar na pasta AppFlowy existente ...", + "folderHintText": "nome da pasta", + "location": "Criando nova pasta", + "locationDesc": "Escolha um nome para sua pasta de dados do AppFlowy", + "browser": "Navegar", + "create": "Criar", + "folderPath": "Caminho para armazenar sua pasta", + "locationCannotBeEmpty": "O caminho não pode estar vazio" + }, + "user": { + "name": "Nome", + "icon": "Ícone", + "selectAnIcon": "Escolha um ícone", + "pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI" + } + }, + "grid": { + "settings": { + "filter": "Filtro", + "sort": "Organizar", + "sortBy": "Organizar por", + "Properties": "Propriedades", + "group": "Grupo", + "addFilter": "Adicionar filtro", + "deleteFilter": "Apagar filtro", + "filterBy": "Filtrar por...", + "typeAValue": "Digite um valor..." + }, + "textFilter": { + "contains": "Contém", + "doesNotContain": "Não contém", + "endsWith": "Termina com", + "startWith": "Inicia com", + "is": "É", + "isNot": "Não é", + "isEmpty": "Está vazio", + "isNotEmpty": "Não está vazio", + "choicechipPrefix": { + "isNot": "Não", + "startWith": "Inicia com", + "endWith": "Termina com", + "isEmpty": "está vazio", + "isNotEmpty": "não está vazio" + } + }, + "checkboxFilter": { + "isChecked": "Marcado", + "isUnchecked": "Desmarcado", + "choicechipPrefix": { + "is": "está" + } + }, + "checklistFilter": { + "isComplete": "está completo", + "isIncomplted": "está imcompleto" + }, + "singleSelectOptionFilter": { + "is": "Está", + "isNot": "Não está", + "isEmpty": "Está vazio", + "isNotEmpty": "Não está vazio" + }, + "multiSelectOptionFilter": { + "contains": "Contém", + "doesNotContain": "Não contém", + "isEmpty": "Está vazio", + "isNotEmpty": "Está vazio" + }, + "field": { + "hide": "Ocultar", + "insertLeft": "Inserir a esquerda", + "insertRight": "Inserir a direita", + "duplicate": "Duplicar", + "delete": "Apagar", + "textFieldName": "Texto", + "checkboxFieldName": "Caixa de seleção", + "dateFieldName": "Data", + "numberFieldName": "Números", + "singleSelectFieldName": "Selecionar", + "multiSelectFieldName": "Multi seleção", + "urlFieldName": "URL", + "checklistFieldName": "Lista", + "numberFormat": "Formato numérico", + "dateFormat": "Formato de data", + "includeTime": "Incluir hora", + "dateFormatFriendly": "Mês Dia, Ano", + "dateFormatISO": "Ano-Mês-Dia", + "dateFormatLocal": "Mês/Dia/Ano", + "dateFormatUS": "Ano/Mês/Dia", + "timeFormat": "Formato de hora", + "invalidTimeFormat": "Formato inválido", + "timeFormatTwelveHour": "12 horas", + "timeFormatTwentyFourHour": "24 horas", + "addSelectOption": "Adicionar uma opção", + "optionTitle": "Opções", + "addOption": "Adicioar opção", + "editProperty": "Editar propriedade", + "newProperty": "Nova coluna", + "deleteFieldPromptMessage": "Tem certeza? Esta propriedade será excluída" + }, + "sort": { + "ascending": "Crescente", + "descending": "Decrescente", + "deleteSort": "Apagar ordenação", + "addSort": "Adicionar ordenação" + }, + "row": { + "duplicate": "Duplicar", + "delete": "Apagar", + "textPlaceholder": "Vazio", + "copyProperty": "Propriedade copiada para a área de transferência", + "count": "Contagem", + "newRow": "Nova linha" + }, + "selectOption": { + "create": "Criar", + "purpleColor": "Roxo", + "pinkColor": "Rosa", + "lightPinkColor": "Rosa claro", + "orangeColor": "Laranja", + "yellowColor": "Amarelo", + "limeColor": "Verde limão", + "greenColor": "Verde", + "aquaColor": "Áqua", + "blueColor": "Azul", + "deleteTag": "Apagar etiqueta", + "colorPannelTitle": "Cores", + "pannelTitle": "Escolha uma opção ou crie uma", + "searchOption": "Procurar uma opção" + }, + "checklist": { + "panelTitle": "Adicionar um item" + }, + "menuName": "Grade" + }, + "document": { + "menuName": "Documento", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "Selecione um quadro para vincular" + }, + "grid": { + "selectAGridToLinkTo": "Selecione um grade para vincular" + } + }, + "plugins": { + "referencedBoard": "Quadro vinculado", + "referencedGrid": "Grade vinculado", + "autoCompletionMenuItemName": "Preenchimento Automático", + "autoGeneratorMenuItemName": "Gerar nome automaticamente", + "autoGeneratorTitleName": "Gerar por IA", + "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 OpenAI", + "smartEditFixSpelling": "Corrigir ortografia", + "smartEditSummarize": "Resumir", + "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI", + "smartEditCouldNotFetchKey": "Não foi possível obter a chave OpenAI" + } + }, + "board": { + "column": { + "create_new_card": "Novo" + }, + "menuName": "Quadro" + }, + "calendar": { + "menuName": "Calendário", + "navigation": { + "today": "Hoje", + "jumpToday": "Pular para hoje", + "previousMonth": "Mês anterior", + "nextMonth": "Próximo mês" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/pt-PT.json b/frontend/appflowy_flutter/assets/translations/pt-PT.json new file mode 100644 index 0000000000..a95d2d6ef2 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/pt-PT.json @@ -0,0 +1,149 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Me", + "welcomeText": "Bem vindo ao @:appName", + "githubStarText": "Star on GitHub", + "subscribeNewsletterText": "Inscreve-te ao Newsletter", + "letsGoButtonText": "Bora", + "title": "Título", + "signUp": { + "buttonText": "Inscreve-te", + "title": "Inscreve-te ao @:appName", + "getStartedText": "Começar", + "emptyPasswordError": "A palavra-passe não pode estar em branco.", + "repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.", + "unmatchedPasswordError": "As palavras-passes não coincidem.", + "alreadyHaveAnAccount": "Já possuis uma conta?", + "emailHint": "Email", + "passwordHint": "Password", + "repeatPasswordHint": "Confirma a tua password" + }, + "signIn": { + "loginTitle": "Entre no @:appName", + "loginButtonText": "Login", + "buttonText": "Entre", + "forgotPassword": "Esqueceste-te da tua palavra-passe?", + "emailHint": "Email", + "passwordHint": "Palavra-passe", + "dontHaveAnAccount": "Não possuis uma conta?", + "repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.", + "unmatchedPasswordError": "As palavras-passes não conferem." + }, + "workspace": { + "create": "Cria um ambiente de trabalho", + "hint": "ambiente de trabalho", + "notFoundError": "Ambiente de trabalho não encontrada" + }, + "shareAction": { + "buttonText": "Partilhar", + "workInProgress": "Em breve", + "markdown": "Markdown", + "copyLink": "Copiar o link" + }, + "disclosureAction": { + "rename": "Renomear", + "delete": "Apagar", + "duplicate": "Duplicar" + }, + "blankPageTitle": "Página em branco", + "newPageText": "Nova página", + "trash": { + "text": "Lixo", + "restoreAll": "Restaurar todos", + "deleteAll": "Apagar todos", + "pageHeader": { + "fileName": "Nome do ficheiro", + "lastModified": "Última modificação", + "created": "Criado" + } + }, + "deletePagePrompt": { + "text": "Esta página está no lixo", + "restore": "Restaurar a página", + "deletePermanent": "Apagar permanentemente" + }, + "dialogCreatePageNameHint": "Nome da página", + "questionBubble": { + "whatsNew": "O que há de novo?", + "help": "Ajuda & Suporte", + "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" + } + }, + "menuAppHeader": { + "addPageTooltip": "Adiciona uma nova página.", + "defaultNewPageName": "Sem título", + "renameDialog": "Renomear" + }, + "toolbar": { + "undo": "Desfazer", + "redo": "Refazer", + "bold": "Negrito", + "italic": "Itálico", + "underline": "Sublinhado", + "strike": "Riscado", + "numList": "Lista numerada", + "bulletList": "Lista com marcadores", + "checkList": "Lista de verificação", + "inlineCode": "Embutir código", + "quote": "Citação em bloco", + "header": "Cabeçalho", + "highlight": "Realçar" + }, + "tooltip": { + "lightMode": "Mudar para o modo Claro.", + "darkMode": "Mudar para o modo Escuro." + }, + "contactsPage": { + "title": "Conctatos", + "whatsHappening": "O que está a acontecer nesta semana?", + "addContact": "Adicionar um conctato", + "editContact": "Editar um conctato" + }, + "button": { + "OK": "OK", + "Cancel": "Cancelar", + "signIn": "Entrar", + "signOut": "Sair", + "complete": "Completar", + "save": "Guardar" + }, + "label": { + "welcome": "Bem vindo!", + "firstName": "Nome", + "middleName": "Nome do Meio", + "lastName": "Apelido", + "stepX": "Passo {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Erro ao conectar à sua conta.", + "failedMsg": "Verifica se concluiste o processo de login no teu navegador." + }, + "google": { + "title": "GOOGLE SIGN-IN", + "instruction1": "Para importar os teus Conctatos do Google, tens de autorizar esta aplicação usando o teu navegador web.", + "instruction2": "Copia este código para a tua área de transferências clicando no ícone ou selecionando o texto:", + "instruction3": "Navega até o link a seguir no seu navegador e digite o código acima:", + "instruction4": "Clica no botão abaixo ao concluir a inscrição:" + } + }, + "settings": { + "title": "Definições", + "menu": { + "appearance": "Aparência", + "language": "Idioma", + "open": "Abrir as Definições" + }, + "appearance": { + "lightLabel": "Modo Claro", + "darkLabel": "Modo Escuro" + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/ru-RU.json b/frontend/appflowy_flutter/assets/translations/ru-RU.json new file mode 100644 index 0000000000..e7ebe7bc07 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/ru-RU.json @@ -0,0 +1,473 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Я", + "welcomeText": "Добро пожаловать в @:appName", + "githubStarText": "Поставить звезду на GitHub", + "subscribeNewsletterText": "Подписаться на рассылку", + "letsGoButtonText": "Начнём", + "title": "Заголовок", + "signUp": { + "buttonText": "Зарегистрироваться", + "title": "Регистрация в @:appName", + "getStartedText": "Начать", + "emptyPasswordError": "Пароль не может быть пустым", + "repeatPasswordEmptyError": "Повтор пароля не может быть пустым", + "unmatchedPasswordError": "Пароли не совпадают", + "alreadyHaveAnAccount": "Уже есть аккаунт?", + "emailHint": "Электронная почта", + "passwordHint": "Пароль", + "repeatPasswordHint": "Повторите пароль" + }, + "signIn": { + "loginTitle": "Войти в @:appName", + "loginButtonText": "Войти", + "buttonText": "Авторизация", + "forgotPassword": "Забыли пароль?", + "emailHint": "Электронная почта", + "passwordHint": "Пароль", + "dontHaveAnAccount": "Нет аккаунта?", + "repeatPasswordEmptyError": "Повтор пароля не может быть пустым", + "unmatchedPasswordError": "Пароли не совпадают" + }, + "workspace": { + "create": "Создать рабочее пространство", + "hint": "рабочее пространство", + "notFoundError": "Нет такого рабочего пространства" + }, + "shareAction": { + "buttonText": "Поделиться", + "workInProgress": "В разработке", + "markdown": "Markdown", + "copyLink": "Скопировать ссылку" + }, + "moreAction": { + "small": "маленький", + "medium": "средний", + "large": "большой", + "fontSize": "Размер шрифта", + "import": "Импортировать", + "moreOptions": "" + }, + "importPanel": { + "textAndMarkdown": "Текст и Markdown", + "documentFromV010": "Документ из v0.1.0", + "databaseFromV010": "База данных из v0.1.0", + "csv": "CSV" + }, + "disclosureAction": { + "rename": "Переименовать", + "delete": "Удалить", + "duplicate": "Дублировать" + }, + "blankPageTitle": "Пустая страница", + "newPageText": "Новая страница", + "trash": { + "text": "Корзина", + "restoreAll": "Восстановить всё", + "deleteAll": "Очистить", + "pageHeader": { + "fileName": "Имя", + "lastModified": "Последнее изменение", + "created": "Создан" + } + }, + "deletePagePrompt": { + "text": "Эта страница в Корзине", + "restore": "Восстановить страницу", + "deletePermanent": "Удалить навсегда" + }, + "dialogCreatePageNameHint": "Имя страницы", + "questionBubble": { + "shortcuts": "Комбинации клавиш", + "whatsNew": "Что нового?", + "help": "Помощь", + "markdown": "Markdown", + "debug": { + "name": "Отладочная информация", + "success": "Скопировано в буфер обмена!", + "fail": "Не получилось скопировать" + } + }, + "menuAppHeader": { + "addPageTooltip": "Быстро добавить новую страницу", + "defaultNewPageName": "Без заголовка", + "renameDialog": "Переименовать" + }, + "toolbar": { + "undo": "Отменить", + "redo": "Повторить", + "bold": "Жирный", + "italic": "Курсив", + "underline": "Подчёркнутый", + "strike": "Зачёркнутый", + "numList": "Нумерованный список", + "bulletList": "Маркированный список", + "checkList": "Список To-Do", + "inlineCode": "Код", + "quote": "Цитата", + "header": "Заголовок", + "highlight": "Выделение", + "color": "Цвет" + }, + "tooltip": { + "lightMode": "Переключить на светлую тему", + "darkMode": "Переключить на тёмную тему", + "openAsPage": "Открыть как страницу", + "addNewRow": "Добавить новую строку", + "openMenu": "Открыть меню", + "dragRow": "Долгое нажатие для изменения порядка строк", + "viewDataBase": "Открыть базу данных", + "referencePage": "Ссылки на {name}" + }, + "sideBar": { + "closeSidebar": "Закрыть боковое меню", + "openSidebar": "Открыть боковое меню" + }, + "notifications": { + "export": { + "markdown": "Заметка экспортирована в Markdown", + "path": "Документы/flowy" + } + }, + "contactsPage": { + "title": "Контакты", + "whatsHappening": "Что нового на этой неделе?", + "addContact": "Добавить контакт", + "editContact": "Редактировать контакт" + }, + "button": { + "OK": "OK", + "Done": "Завершить", + "Cancel": "Отмена", + "signIn": "Войти", + "signOut": "Выйти", + "complete": "Завершить", + "save": "Сохранить", + "generate": "Сгенерировать", + "esc": "ESC", + "keep": "Оставить", + "tryAgain": "Повторить", + "discard": "Отменить", + "replace": "Заменить", + "insertBelow": "Вставить ниже" + }, + "label": { + "welcome": "Добро пожаловать!", + "firstName": "Имя", + "middleName": "Отчество", + "lastName": "Фамилия", + "stepX": "Этап {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Не удалось подключиться к вашей учетной записи.", + "failedMsg": "Убедитесь, что вы завершили вход в своём браузере." + }, + "google": { + "title": "Вход через Google", + "instruction1": "Чтобы импортировать ваши Google Контакты, вам нужно будет авторизовать приложение через браузер.", + "instruction2": "Скопируйте этот код в буфер обмена (нажав кнопку или выделив текст):", + "instruction3": "Пройдите по ссылке и введите этот код:", + "instruction4": "Нажмите на кнопку ниже, когда завершите вход:" + } + }, + "settings": { + "title": "Настройки", + "menu": { + "appearance": "Внешний вид", + "language": "Язык", + "user": "Пользователь", + "files": "Файлы", + "open": "Открыть настройки" + }, + "appearance": { + "themeMode": { + "label": "Тема приложения", + "light": "Светлая", + "dark": "Тёмная", + "system": "Системная" + }, + "theme": "Тема" + }, + "files": { + "copy": "Копировать", + "defaultLocation": "Где сейчас хранятся ваши данные", + "exportData": "Экспорт данных", + "doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь", + "restoreLocation": "Восстановить путь AppFlowy по умолчанию", + "customizeLocation": "Открыть другую папку", + "restartApp": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.", + "exportDatabase": "Экспорт базы данных", + "selectFiles": "Выбрать файлы, которые необходимо экспортировать", + "selectAll": "Выбрать всё", + "deselectAll": "Снять выделение", + "createNewFolder": "Создать новую папку", + "createNewFolderDesc": "Указать, где хранить свои данные ...", + "defineWhereYourDataIsStored": "Указать хранилище данных", + "open": "Открыть", + "openFolder": "Открыть существующую папку", + "openFolderDesc": "Чтение и запись в существующую папку AppFlowy ...", + "folderHintText": "имя папки", + "location": "Создание новой папки", + "locationDesc": "Выбрать имя папки данных AppFlowy", + "browser": "Обзор", + "create": "Создать", + "set": "Set", + "folderPath": "Путь к вашей папке", + "locationCannotBeEmpty": "Путь не может быть пустым", + "pathCopiedSnackbar": "Путь скопирован в буфер обмена!", + "changeLocationTooltips": "Сменить местоположения", + "change": "Изменить", + "openLocationTooltips": "Открыть другую директорию данных", + "recoverLocationTooltips": "Сбросить к местоположению по умолчанию", + "exportFileSuccess": "Экспорт завершён!", + "exportFileFail": "Не удалость экспортировать!", + "export": "Экспорт" + }, + "user": { + "name": "Имя", + "icon": "Иконка", + "selectAnIcon": "Выбрать иконку", + "pleaseInputYourOpenAIKey": "Введите токен OpenAI" + } + }, + "grid": { + "settings": { + "filter": "Фильтр", + "sort": "Сортировать", + "sortBy": "Сортировать по", + "Properties": "Свойства", + "group": "Группировать", + "addFilter": "Добавить фильтр", + "deleteFilter": "Удалить фильтр", + "filterBy": "Фильтровать по...", + "typeAValue": "Введите значение...", + "layout": "Вид", + "databaseLayout": "Вид базы данных" + }, + "textFilter": { + "contains": "Содержит", + "doesNotContain": "Не содержит", + "endsWith": "Заканчивается на", + "startWith": "Начинается с", + "is": "Является", + "isNot": "Не является", + "isEmpty": "Пусто", + "isNotEmpty": "Не пусто", + "choicechipPrefix": { + "isNot": "Не является", + "startWith": "Начинается с", + "endWith": "Заканчивается на", + "isEmpty": "пусто", + "isNotEmpty": "не пусто" + } + }, + "checkboxFilter": { + "isChecked": "Отмечено", + "isUnchecked": "Не отмечено", + "choicechipPrefix": { + "is": "является" + } + }, + "checklistFilter": { + "isComplete": "завершено", + "isIncomplted": "не завершено" + }, + "singleSelectOptionFilter": { + "is": "Является", + "isNot": "Не является", + "isEmpty": "Пусто", + "isNotEmpty": "Не пусто" + }, + "multiSelectOptionFilter": { + "contains": "Содержит", + "doesNotContain": "Не содержит", + "isEmpty": "Пусто", + "isNotEmpty": "Не пусто" + }, + "field": { + "hide": "Скрыть", + "insertLeft": "Вставить слева", + "insertRight": "Вставить справа", + "duplicate": "Дублировать", + "delete": "Удалить", + "textFieldName": "Текст", + "checkboxFieldName": "Чекбокс", + "dateFieldName": "Дата", + "updatedAtFieldName": "Последнее изменение", + "createdAtFieldName": "Создано", + "numberFieldName": "Число", + "singleSelectFieldName": "Выбор", + "multiSelectFieldName": "Выбор нескольких", + "urlFieldName": "URL", + "checklistFieldName": "Контрольный список", + "numberFormat": "Формат числа", + "dateFormat": "Формат даты", + "includeTime": "Время", + "dateFormatFriendly": "День Месяц, Год", + "dateFormatISO": "Год-Месяц-День", + "dateFormatLocal": "Месяц/День/Год", + "dateFormatUS": "Год/Месяц/День", + "dateFormatDayMonthYear": "День/Mесяц/Год", + "timeFormat": "Формат времени", + "invalidTimeFormat": "Неверный формат", + "timeFormatTwelveHour": "12 часов", + "timeFormatTwentyFourHour": "24 часа", + "addSelectOption": "Добавить вариант", + "optionTitle": "Варианты", + "addOption": "Добавить", + "editProperty": "Редактировать свойство", + "newProperty": "Добавить колонку", + "deleteFieldPromptMessage": "Вы уверены? Свойство будет удалено" + }, + "sort": { + "ascending": "По возрастанию", + "descending": "По убыванию", + "deleteSort": "Удалить сортировку", + "addSort": "Добавить сортировку" + }, + "row": { + "duplicate": "Дублировать", + "delete": "Удалить", + "textPlaceholder": "Пусто", + "copyProperty": "Свойство скопировано", + "count": "Количество", + "newRow": "Новая строка", + "action": "Действия" + }, + "selectOption": { + "create": "Создать", + "purpleColor": "Фиолетовый", + "pinkColor": "Розовый", + "lightPinkColor": "Светло-розовый", + "orangeColor": "Оранжевый", + "yellowColor": "Желтый", + "limeColor": "Ярко-зелёный", + "greenColor": "Зелёный", + "aquaColor": "Бирюзовый", + "blueColor": "Синий", + "deleteTag": "Удалить вариант", + "colorPanelTitle": "Цвета", + "panelTitle": "Выберите или создайте вариант", + "searchOption": "Поиск" + }, + "checklist": { + "panelTitle": "Добавить элемент" + }, + "menuName": "Сетка", + "referencedGridPrefix": "Просмотр" + }, + "document": { + "menuName": "Документ", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "Выбрать доску", + "createANewBoard": "Создать доску" + }, + "grid": { + "selectAGridToLinkTo": "Выберите сетку", + "createANewGrid": "Создать сетку" + }, + "calendar": { + "selectACalendarToLinkTo": "Выбрать календарь", + "createANewCalendar": "Создать календарь" + } + }, + "plugins": { + "referencedBoard": "Связанные доски", + "referencedGrid": "Связанные сетки", + "autoGeneratorMenuItemName": "Генератор OpenAI", + "autoGeneratorTitleName": "OpenAI: попросить ИИ написать что угодно...", + "autoGeneratorLearnMore": "Узнать больше", + "autoGeneratorGenerate": "Генерировать", + "autoGeneratorHintText": "Спросить OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "Не могу получить токен OpenAI", + "smartEdit": "ИИ ассистенты", + "openAI": "OpenAI", + "smartEditFixSpelling": "Исправить правописание", + "warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.", + "smartEditSummarize": "Выделить суть", + "smartEditImproveWriting": "Улучшить", + "smartEditMakeLonger": "Продолжить", + "smartEditCouldNotFetchResult": "Не могу получить ответ от OpenAI", + "smartEditCouldNotFetchKey": "Не могу получить токен OpenAI", + "smartEditDisabled": "Подключить OpenAI", + "discardResponse": "Хотите убрать ответы ИИ?", + "cover": { + "changeCover": "Сменить обложку", + "colors": "Цвета", + "images": "Изображения", + "clearAll": "Очистить", + "abstract": "Абстракные", + "addCover": "Добавить обложку", + "addLocalImage": "Добавить изображение с диска", + "invalidImageUrl": "Некорректная ссылка на изображение", + "failedToAddImageToGallery": "Ошибка добавления изображения в галерею", + "enterImageUrl": "Введите ссылку на изображение", + "add": "Добавить", + "back": "Назад", + "saveToGallery": "Сохранить в галерею", + "removeIcon": "Удалить иконку", + "pasteImageUrl": "Вставить ссылку на изображение", + "or": "ИЛИ", + "pickFromFiles": "Выбрать с диска", + "couldNotFetchImage": "Не удалось получить изображение", + "imageSavingFailed": "Не удалось сохранить изображение", + "addIcon": "Добавить иконку", + "coverRemoveAlert": "Изображение будет удалено с обложки", + "alertDialogConfirmation": "Вы хотите продолжить?" + }, + "mathEquation": { + "addMathEquation": "Добавить математическое выражение", + "editMathEquation": "Редактировать математическое выражение" + }, + "optionAction": { + "click": "Кликните", + "toOpenMenu": " чтобы открыть меню", + "delete": "Удалить", + "duplicate": "Дублировать", + "turnInto": "Превратить", + "moveUp": "Поднять", + "moveDown": "Опустить", + "color": "Цвет", + "align": "Выравнивание", + "left": "По левому краю", + "center": "По центру", + "right": "По правому краю", + "defaultColor": "Цвет по умолчанию" + } + } + }, + "board": { + "column": { + "create_new_card": "Создать" + }, + "menuName": "Доска", + "referencedBoardPrefix": "Просмотр" + }, + "calendar": { + "menuName": "Календарь", + "defaultNewCalendarTitle": "Безымянный", + "navigation": { + "today": "Сегодня", + "jumpToday": "Перейти к сегодняшнему дню", + "previousMonth": "Предыдущий месяц", + "nextMonth": "Следующий месяц" + }, + "settings": { + "showWeekNumbers": "Показывать номера недель", + "showWeekends": "Показывать выходные", + "firstDayOfWeek": "Первый день недели", + "layoutDateField": "Вид календаря", + "noDateTitle": "No Date", + "noDateHint": "Unscheduled events will show up here", + "clickToAdd": "Click to add to the calendar", + "name": "Calendar layout" + }, + "referencedCalendarPrefix": "Вид" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/sv.json b/frontend/appflowy_flutter/assets/translations/sv.json new file mode 100644 index 0000000000..b6bb8c7ab7 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/sv.json @@ -0,0 +1,239 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Jag", + "welcomeText": "Välkommen till @:appName", + "githubStarText": "Stärnmärk på GitHub", + "subscribeNewsletterText": "Prenumerera på nyhetsbrev", + "letsGoButtonText": "Kör igång", + "title": "Namn", + "signUp": { + "buttonText": "Registrera dig", + "title": "Registrera dig på @:appName", + "getStartedText": "Sätt igång", + "emptyPasswordError": "Lösenordet kan inte vara tomt", + "repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt", + "unmatchedPasswordError": "Upprepat lösenord är inte samma som det första", + "alreadyHaveAnAccount": "Har du redan ett konto?", + "emailHint": "E-post", + "passwordHint": "Lösenord", + "repeatPasswordHint": "Uprepa lösenordet" + }, + "signIn": { + "loginTitle": "Logga in till @:appName", + "loginButtonText": "Logga in", + "buttonText": "Registrering", + "forgotPassword": "Glömt lösenordet?", + "emailHint": "E-post", + "passwordHint": "Lösenord", + "dontHaveAnAccount": "Har du inget konto?", + "repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt", + "unmatchedPasswordError": "Upprepat lösenord är inte samma som det första" + }, + "workspace": { + "create": "Skapa arbetsyta", + "hint": "Arbetsyta", + "notFoundError": "Hittade ingen arbetsyta" + }, + "shareAction": { + "buttonText": "Dela", + "workInProgress": "Kommer snart", + "markdown": "Markdown", + "copyLink": "Kopiera länk" + }, + "disclosureAction": { + "rename": "Byt namn", + "delete": "Ta bort", + "duplicate": "Klona" + }, + "blankPageTitle": "Tom sida", + "newPageText": "Ny sida", + "trash": { + "text": "Skräp", + "restoreAll": "Återställ alla", + "deleteAll": "Ta bort alla", + "pageHeader": { + "fileName": "Filnamn", + "lastModified": "Ändrad", + "created": "Skapad" + } + }, + "deletePagePrompt": { + "text": "Denna sida är i skräpmappen", + "restore": "Återställ sida", + "deletePermanent": "Radera permanent" + }, + "dialogCreatePageNameHint": "Sidnamn", + "questionBubble": { + "whatsNew": "Vad nytt?", + "help": "Hjälp & Support", + "debug": { + "name": "Felsökningsinfo", + "success": "Kopierade felsökningsinfo till urklipp!", + "fail": "Kunde inte kopiera felsökningsinfo till urklipp" + } + }, + "menuAppHeader": { + "addPageTooltip": "Lägg snabbt till en sida inuti", + "defaultNewPageName": "Namnlös", + "renameDialog": "Byt namn" + }, + "toolbar": { + "undo": "Ångra", + "redo": "Upprepa", + "bold": "Fet", + "italic": "Kursiv", + "underline": "Understruken", + "strike": "Genomstruken", + "numList": "Numrerad lista", + "bulletList": "Punktlista", + "checkList": "Checklista", + "inlineCode": "Infogad kod", + "quote": "Citatblock", + "header": "Rubrik", + "highlight": "Färgmarkera" + }, + "tooltip": { + "lightMode": "Växla till ljust läge", + "darkMode": "Växla till mörkt läge", + "openAsPage": "Öppna som sida", + "addNewRow": "Lägg till ny rad", + "openMenu": "Klicka för att öppna meny" + }, + "sideBar": { + "closeSidebar": "Stäng sidofältet", + "openSidebar": "Öppna sidofältet" + }, + "notifications": { + "export": { + "markdown": "Exporterade anteckning till Markdown", + "path": "Dokument/flowy" + } + }, + "contactsPage": { + "title": "Kontakter", + "whatsHappening": "Vad händer denna vecka?", + "addContact": "Lägg till kontakt", + "editContact": "Redigera kontakt" + }, + "button": { + "OK": "OK", + "Cancel": "Avbryt", + "signIn": "Logga in", + "signOut": "Logga ut", + "complete": "Slutfört", + "save": "Spara" + }, + "label": { + "welcome": "Välkommen!", + "firstName": "Förnamn", + "middleName": "Mellannamn", + "lastName": "Efternamn", + "stepX": "Steg {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Kan inte ansluta till ditt konto.", + "failedMsg": "Tillse att du har slutfört registreringsprocessen i din webbläsare." + }, + "google": { + "title": "GOOGLE-inloggning", + "instruction1": "För att kunna importera dina Google-kontakter, måste du auktorisera detta program med hjälp av din webbläsare.", + "instruction2": "Kopiera den här koden till urklipp genom att klicka på ikonen eller genom att markera texten:", + "instruction3": "Gå till följande länk i din webbläsare, och ange ovanstående kod:", + "instruction4": "Tryck på nedanstående knapp när du slutfört registreringen:" + } + }, + "settings": { + "title": "Inställningar", + "menu": { + "appearance": "Utseende", + "language": "Språk", + "user": "Användare", + "open": "Öppna inställningarna" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Ljust läge", + "dark": "Mörkt läge", + "system": "Adapt to System" + } + } + }, + "grid": { + "settings": { + "filter": "Filter", + "sortBy": "Sortera efter", + "Properties": "Egenskaper", + "group": "Grupp" + }, + "field": { + "hide": "Dölj", + "insertLeft": "Infoga till vänster", + "insertRight": "Infoga till höger", + "duplicate": "Klona", + "delete": "Ta bort", + "textFieldName": "Text", + "checkboxFieldName": "Checkruta", + "dateFieldName": "Datum", + "numberFieldName": "Siffror", + "singleSelectFieldName": "Välj", + "multiSelectFieldName": "Välj flera", + "urlFieldName": "URL", + "numberFormat": "Sifferformat", + "dateFormat": "Datumformat", + "includeTime": "Inkludera tid", + "dateFormatFriendly": "Månad Dag, År", + "dateFormatISO": "År-Månad-Dag", + "dateFormatLocal": "Månad/Dag/År", + "dateFormatUS": "År/Månad/Dag", + "timeFormat": "Tidsformat", + "invalidTimeFormat": "Ogiltigt format", + "timeFormatTwelveHour": "12-timmars", + "timeFormatTwentyFourHour": "24-timmars", + "addSelectOption": "Lägg till ett alternativ", + "optionTitle": "Alternativ", + "addOption": "Lägg till alternativ", + "editProperty": "Redigera egenskap", + "newProperty": "Ny kolumn", + "deleteFieldPromptMessage": "Är du säker? Denna egenskap kommer att raderas." + }, + "row": { + "duplicate": "Klona", + "delete": "Ta bort", + "textPlaceholder": "Tom", + "copyProperty": "Kopierade egenskap till urklipp", + "count": "Antal", + "newRow": "Ny rad" + }, + "selectOption": { + "create": "Skapa", + "purpleColor": "Purpur", + "pinkColor": "Rosa", + "lightPinkColor": "Ljusrosa", + "orangeColor": "Orange", + "yellowColor": "Gul", + "limeColor": "Lime", + "greenColor": "Grön", + "aquaColor": "Vatten", + "blueColor": "Blå", + "deleteTag": "Ta bort tagg", + "colorPanelTitle": "Färger", + "panelTitle": "Välj ett alternativ eller skapa ett", + "searchOption": "Sök efter ett alternativ" + }, + "menuName": "Tabell" + }, + "document": { + "menuName": "Dokument", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + } + }, + "board": { + "column": { + "create_new_card": "Nytt" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/tr-TR.json b/frontend/appflowy_flutter/assets/translations/tr-TR.json new file mode 100644 index 0000000000..5495c32102 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/tr-TR.json @@ -0,0 +1,153 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Ben", + "welcomeText": "@:appName'e Hoş Geldiniz!", + "githubStarText": "GitHub Yıldızı!", + "subscribeNewsletterText": "Bültene Abone Ol", + "letsGoButtonText": "Hadi başlayalım.", + "title": "Başlık", + "signUp": { + "buttonText": "Kayıt Ol", + "title": "@:appName'e kaydolun.", + "getStartedText": "başlayalım", + "emptyPasswordError": "Parola boş olamaz", + "repeatPasswordEmptyError": "Parola (tekrar) boş olamaz", + "unmatchedPasswordError": "Parolalar eşleşmiyor", + "alreadyHaveAnAccount": "Zaten hesabınız var mı?", + "emailHint": "E-Posta", + "passwordHint": "Parola", + "repeatPasswordHint": "Tekrar parola" + }, + "signIn": { + "loginTitle": "@:appName oturum aç", + "loginButtonText": "Giriş", + "buttonText": "Oturum Aç", + "forgotPassword": "Parolanızı mı Unuttunuz?", + "emailHint": "E-Posta", + "passwordHint": "Parola", + "dontHaveAnAccount": "Hesabınız yok mu?", + "repeatPasswordEmptyError": "Parola (tekrar) boş olamaz", + "unmatchedPasswordError": "Parolalar eşleşmiyor" + }, + "workspace": { + "create": "Çalışma alanı oluştur", + "hint": "Çalışma alanı", + "notFoundError": "Çalışma alanı bulunamadı" + }, + "shareAction": { + "buttonText": "Paylaş", + "workInProgress": "Yakında", + "markdown": "Markdown", + "copyLink": "Link'i Kopyala" + }, + "disclosureAction": { + "rename": "Yeniden adlandır", + "delete": "Sil", + "duplicate": "Çoğalt" + }, + "blankPageTitle": "Boş sayfa", + "newPageText": "Yeni sayfa", + "trash": { + "text": "Çöp", + "restoreAll": "Geri Yükle", + "deleteAll": "Sil", + "pageHeader": { + "fileName": "Dosya adı", + "lastModified": "Son Değiştirme", + "created": "Oluşturuldu" + } + }, + "deletePagePrompt": { + "text": "Bu sayfa Çöp Kutusu'nda", + "restore": "Sayfayı geri yükle", + "deletePermanent": "Kalıcı olarak sil" + }, + "dialogCreatePageNameHint": "Sayfa adı", + "questionBubble": { + "whatsNew": "Yeni ne var?", + "help": "Yardım & Destek", + "debug": { + "name": "Hata Ayıklama", + "success": "Hata ayıklama bilgileri panoya kopyalandı!", + "fail": "Hata ayıklama bilgileri panoya kopyalanamıyor" + } + }, + "menuAppHeader": { + "addPageTooltip": "Yeni bir sayfa ekleyin", + "defaultNewPageName": "Başlıksız", + "renameDialog": "Yeniden adlandır" + }, + "toolbar": { + "undo": "Geri", + "redo": "İleri", + "bold": "Kalın", + "italic": "İtalik", + "underline": "Altı Çizili", + "strike": "Üstü Çizili", + "numList": "Numaralı Liste", + "bulletList": "Madde İşaretli Liste", + "checkList": "Yapılacaklar Listesi", + "inlineCode": "Kod", + "quote": "Alıntı", + "header": "Başlık", + "highlight": "Vurgu" + }, + "tooltip": { + "lightMode": "Aydınlık Mod'a Geç", + "darkMode": "Karanlık Mod'a Geç" + }, + "contactsPage": { + "title": "İletişim", + "whatsHappening": "Bu hafta neler var?", + "addContact": "Kişi Ekle", + "editContact": "Kişiyi Düzenle" + }, + "button": { + "OK": "TAMAM", + "Cancel": "İptal", + "signIn": "Oturum Aç", + "signOut": "Oturum Kapat", + "complete": "Tamamlandı", + "save": "Kaydet" + }, + "label": { + "welcome": "Merhaba!", + "firstName": "Ad", + "middleName": "İkinci Ad", + "lastName": "Soyad", + "stepX": "Aşama {X}" + }, + "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." + }, + "google": { + "title": "GOOGLE OTURUM AÇMA", + "instruction1": "Google Kişilerinizi içe aktarmak için web tarayıcınızı kullanarak bu uygulamaya izin vermeniz gerekir.", + "instruction2": "Simgeyi tıklayarak veya metni seçerek bu kodu panonuza kopyalayın:", + "instruction3": "Web tarayıcınızda aşağıdaki bağlantıyı açın ve yukarıdaki kodu girin:", + "instruction4": "Kayıt işlemini tamamladığınızda aşağıdaki düğmeye basın:" + } + }, + "settings": { + "title": "Ayarlar", + "menu": { + "appearance": "Görünüm", + "language": "Dil", + "open": "Ayarları Aç" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "Aydınlık Mod", + "dark": "Karanlık Mod", + "system": "Adapt to System" + } + } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/zh-CN.json b/frontend/appflowy_flutter/assets/translations/zh-CN.json new file mode 100644 index 0000000000..674dbb3898 --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/zh-CN.json @@ -0,0 +1,240 @@ +{ + "appName": "Appflowy", + "defaultUsername": "我", + "welcomeText": "欢迎使用 @:appName", + "githubStarText": "Star on GitHub", + "subscribeNewsletterText": "消息订阅", + "letsGoButtonText": "开始", + "title": "标题", + "signUp": { + "buttonText": "注册", + "title": "注册 @:appName 账户", + "getStartedText": "开始", + "emptyPasswordError": "密码不能为空", + "repeatPasswordEmptyError": "确认密码不能为空", + "unmatchedPasswordError": "两次密码输入不一致", + "alreadyHaveAnAccount": "已有账户?", + "emailHint": "邮箱", + "passwordHint": "密码", + "repeatPasswordHint": "确认密码" + }, + "signIn": { + "loginTitle": "登录 @:appName", + "loginButtonText": "登录", + "buttonText": "登录", + "forgotPassword": "忘记密码?", + "emailHint": "邮箱", + "passwordHint": "密码", + "dontHaveAnAccount": "没有已注册的账户?", + "repeatPasswordEmptyError": "确认密码不能为空", + "unmatchedPasswordError": "两次密码输入不一致" + }, + "workspace": { + "create": "新建空间", + "hint": "空间", + "notFoundError": "未知的空间" + }, + "shareAction": { + "buttonText": "分享", + "workInProgress": "敬请期待", + "markdown": "Markdown", + "copyLink": "复制链接" + }, + "disclosureAction": { + "rename": "重命名", + "delete": "删除", + "duplicate": "复制" + }, + "blankPageTitle": "空白页", + "newPageText": "新页面", + "trash": { + "text": "回收站", + "restoreAll": "全部恢复", + "deleteAll": "全部删除", + "pageHeader": { + "fileName": "文件名", + "lastModified": "最近修改", + "created": "创建" + } + }, + "deletePagePrompt": { + "text": "此页面已被移动至回收站", + "restore": "恢复页面", + "deletePermanent": "彻底删除" + }, + "dialogCreatePageNameHint": "页面名称", + "questionBubble": { + "whatsNew": "新功能?", + "help": "帮助 & 支持", + "debug": { + "name": "调试信息", + "success": "将调试信息复制到剪贴板!", + "fail": "无法将调试信息复制到剪贴板" + } + }, + "menuAppHeader": { + "addPageTooltip": "在其中快速添加页面", + "defaultNewPageName": "未命名页面", + "renameDialog": "重命名" + }, + "toolbar": { + "undo": "撤销", + "redo": "恢复", + "bold": "加粗", + "italic": "斜体", + "underline": "下划线", + "strike": "删除线", + "numList": "有序列表", + "bulletList": "无序列表", + "checkList": "任务列表", + "inlineCode": "内联代码", + "quote": "块引用", + "header": "标题", + "highlight": "高亮" + }, + "tooltip": { + "lightMode": "切换到亮色模式", + "darkMode": "切换到暗色模式", + "openAsPage": "作为页面打开", + "addNewRow": "增加一行", + "openMenu": "点击打开菜单" + }, + "sideBar": { + "openSidebar": "打开侧边栏", + "closeSidebar": "关闭侧边栏" + }, + "notifications": { + "export": { + "markdown": "导出笔记为Markdown文档", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "联系人", + "whatsHappening": "这周发生了哪些事?", + "addContact": "添加联系人", + "editContact": "编辑联系人" + }, + "button": { + "OK": "确认", + "Cancel": "取消", + "signIn": "登录", + "signOut": "登出", + "complete": "完成", + "save": "保存" + }, + "label": { + "welcome": "欢迎!", + "firstName": "名", + "middleName": "中间名", + "lastName": "姓", + "stepX": "第{X}步" + }, + "oAuth": { + "err": { + "failedTitle": "无法连接到您的账户。", + "failedMsg": "请确认您已在浏览器中完成登录。" + }, + "google": { + "title": "Google 账号登录", + "instruction1": "为了导入您的 Google 联系人,您需要在浏览器中给予本程序授权。", + "instruction2": "单击图标或选择文本复制到剪贴板:", + "instruction3": "进入下面的链接,然后输入上面的代码:", + "instruction4": "完成注册后,点击下面的按钮:" + } + }, + "settings": { + "title": "设置", + "menu": { + "appearance": "外观", + "language": "语言", + "user": "用户", + "open": "打开设置" + }, + "appearance": { + "themeMode": { + "label": "Theme Mode", + "light": "日间模式", + "dark": "夜间模式", + "system": "Adapt to System" + } + } + }, + "grid": { + "settings": { + "filter": "过滤器", + "sortBy": "排序", + "Properties": "属性", + "group": "组" + }, + "field": { + "hide": "隐藏", + "insertLeft": "左侧插入", + "insertRight": "右侧插入", + "duplicate": "拷贝", + "delete": "删除", + "textFieldName": "文本", + "checkboxFieldName": "勾选框", + "dateFieldName": "日期", + "numberFieldName": "数字", + "singleSelectFieldName": "单项选择器", + "multiSelectFieldName": "多项选择器", + "urlFieldName": "链接", + "numberFormat": "数字格式", + "dateFormat": "日期格式", + "includeTime": "包含时间", + "dateFormatFriendly": "月 日, 年", + "dateFormatISO": "年-月-日", + "dateFormatLocal": "月/日/年", + "dateFormatUS": "年/月/日", + "timeFormat": "时间格式", + "invalidTimeFormat": "时间格式错误", + "timeFormatTwelveHour": "12小时制", + "timeFormatTwentyFourHour": "24小时制", + "addSelectOption": "添加一个标签", + "optionTitle": "标签", + "addOption": "添加标签", + "editProperty": "编辑列属性", + "newProperty": "增加一列", + "deleteFieldPromptMessage": "确定要删除这个属性吗? " + }, + "row": { + "duplicate": "复制", + "delete": "删除", + "textPlaceholder": "空", + "copyProperty": "复制列", + "count": "数量", + "newRow": "添加一行" + }, + "selectOption": { + "create": "新建", + "purpleColor": "紫色", + "pinkColor": "粉色", + "lightPinkColor": "浅粉色", + "orangeColor": "橙色", + "yellowColor": "黄色", + "limeColor": "鲜绿色", + "greenColor": "绿色", + "aquaColor": "水蓝色", + "blueColor": "蓝色", + "deleteTag": "删除标签", + "colorPanelTitle": "颜色", + "panelTitle": "选择或新建一个标签", + "searchOption": "搜索标签" + }, + "menuName": "网格" + }, + "document": { + "menuName": "文档", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + } + }, + "board": { + "column": { + "create_new_card": "新建" + }, + "menuName": "看板" + } +} \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/zh-TW.json b/frontend/appflowy_flutter/assets/translations/zh-TW.json new file mode 100644 index 0000000000..355f137d6b --- /dev/null +++ b/frontend/appflowy_flutter/assets/translations/zh-TW.json @@ -0,0 +1,410 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "我", + "welcomeText": "歡迎使用 @:appName", + "githubStarText": "在 GitHub 點星", + "subscribeNewsletterText": "訂閱電子報", + "letsGoButtonText": "出發吧", + "title": "標題", + "signUp": { + "buttonText": "註冊", + "title": "註冊 @:appName", + "getStartedText": "開始使用", + "emptyPasswordError": "密碼不能為空", + "repeatPasswordEmptyError": "確認密碼不能為空", + "unmatchedPasswordError": "確認密碼與密碼不符", + "alreadyHaveAnAccount": "已經有帳號了嗎?", + "emailHint": "電子郵件地址", + "passwordHint": "密碼", + "repeatPasswordHint": "確認密碼" + }, + "signIn": { + "loginTitle": "登入 @:appName", + "loginButtonText": "登入", + "buttonText": "登入", + "forgotPassword": "忘記密碼?", + "emailHint": "電子郵件地址", + "passwordHint": "密碼", + "dontHaveAnAccount": "沒有帳號?", + "repeatPasswordEmptyError": "確認密碼不能為空", + "unmatchedPasswordError": "確認密碼與密碼不符" + }, + "workspace": { + "create": "建立工作區", + "hint": "工作區", + "notFoundError": "找不到工作區" + }, + "shareAction": { + "buttonText": "分享", + "workInProgress": "即將推出", + "markdown": "Markdown", + "copyLink": "複製連結" + }, + "moreAction": { + "small": "小", + "medium": "中", + "large": "大", + "fontSize": "字體大小", + "import": "匯入" + }, + "disclosureAction": { + "rename": "重新命名", + "delete": "刪除", + "duplicate": "複製" + }, + "blankPageTitle": "空白頁面", + "newPageText": "新頁面", + "trash": { + "text": "垃圾筒", + "restoreAll": "全部復原", + "deleteAll": "全部刪除", + "pageHeader": { + "fileName": "檔案名稱", + "lastModified": "最後修改時間", + "created": "建立時間" + } + }, + "deletePagePrompt": { + "text": "此頁面在垃圾筒中", + "restore": "復原頁面", + "deletePermanent": "永久刪除" + }, + "dialogCreatePageNameHint": "頁面名稱", + "questionBubble": { + "shortcuts": "快捷鍵", + "whatsNew": "新功能", + "help": "幫助 & 支援", + "debug": { + "name": "除錯資訊", + "success": "已將除錯資訊複製至剪貼簿!", + "fail": "無法將除錯資訊複製至剪貼簿" + } + }, + "menuAppHeader": { + "addPageTooltip": "快速新增頁面", + "defaultNewPageName": "未命名", + "renameDialog": "重新命名" + }, + "toolbar": { + "undo": "復原", + "redo": "取消復原", + "bold": "粗體", + "italic": "斜體", + "underline": "底線", + "strike": "刪除線", + "numList": "有序清單", + "bulletList": "無序清單", + "checkList": "核取清單", + "inlineCode": "程式碼", + "quote": "區塊引言", + "header": "標題", + "highlight": "反白", + "color": "顏色" + }, + "tooltip": { + "lightMode": "切換至亮色模式", + "darkMode": "切換至暗色模式", + "openAsPage": "以頁面開啓", + "addNewRow": "新增列表", + "openMenu": "點擊開啓選單", + "viewDataBase": "查看資料庫", + "referencePage": "這個 {name} 已參照" + }, + "sideBar": { + "closeSidebar": "關閉側邊欄", + "openSidebar": "開啓側邊欄" + }, + "notifications": { + "export": { + "markdown": "已將筆記匯出成 Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "聯絡人", + "whatsHappening": "這周有甚麼新鮮事?", + "addContact": "新增聯絡人", + "editContact": "編輯聯絡人" + }, + "button": { + "OK": "OK", + "Cancel": "取消", + "signIn": "登入", + "signOut": "登出", + "complete": "完成", + "save": "儲存", + "generate": "產生", + "esc": "離開", + "keep": "保存", + "tryAgain": "再試一次", + "discard": "放棄變更", + "replace": "取代", + "insertBelow": "在下面插入" + }, + "label": { + "welcome": "歡迎!", + "firstName": "名", + "middleName": "中間名", + "lastName": "姓", + "stepX": "步驟 {X}" + }, + "oAuth": { + "err": { + "failedTitle": "無法連接至您的帳號。", + "failedMsg": "請確認您已在瀏覽器中完成登入程序:" + }, + "google": { + "title": "GOOGLE 登入", + "instruction1": "若要匯入您的 Google 聯絡人,您必須透過瀏覽器授權此應用程式:", + "instruction2": "點擊圖示或選取文字以複製代碼:", + "instruction3": "前往下列網址,並輸入上述代碼:", + "instruction4": "完成註冊後,請點擊下方按鈕:" + } + }, + "settings": { + "title": "設定", + "menu": { + "appearance": "外觀", + "language": "語言", + "user": "使用者", + "files": "檔案", + "open": "開啟設定" + }, + "appearance": { + "themeMode": { + "label": "主題模式", + "light": "亮色模式", + "dark": "暗色模式", + "system": "系統設定" + }, + "theme": "主題" + }, + "files": { + "defaultLocation": "Appflowy 資料儲存位置", + "doubleTapToCopy": "雙擊以複製路徑", + "restoreLocation": "回復 Appflowy 預設路徑", + "customizeLocation": "開啓其他資料夾", + "restartApp": "請重新啓動以讓更動生效", + "exportDatabase": "匯出資料庫", + "selectFiles": "選擇需要匯出的檔案", + "createNewFolder": "建立新檔案", + "createNewFolderDesc": "選擇你想儲存資料的位置", + "open": "打開", + "openFolder": "開啓一個已經存在的資料夾", + "openFolderDesc": "讀寫已存在的 AppFlowy 資料夾", + "folderHintText": "資料夾名稱", + "location": "建立新資料夾", + "locationDesc": "命名 Appflowy 資料夾", + "browser": "瀏覽", + "create": "建立", + "folderPath": "儲存資料夾的路徑", + "locationCannotBeEmpty": "路徑不能爲空", + "pathCopiedSnackbar": "檔案儲存空間的路徑已被複製到剪貼簿!" + }, + "user": { + "name": "名稱", + "icon": "圖標", + "selectAnIcon": "選擇圖標", + "pleaseInputYourOpenAIKey": "請輸入你的 OpenAI 密鑰" + } + }, + "grid": { + "settings": { + "filter": "篩選", + "sort": "排序", + "sortBy": "排序方式", + "Properties": "內容", + "group": "群組", + "addFilter": "增加", + "deleteFilter": "刪除篩選器", + "filterBy": "以...篩選", + "typeAValue": "輸入一個值...", + "layout": "佈局" + }, + "textFilter": { + "contains": "包含", + "doesNotContain": "不包含", + "endsWith": "以...結尾", + "startWith": "以...開頭", + "is": "是", + "isNot": "不是", + "isEmpty": "爲空", + "isNotEmpty": "不爲空", + "choicechipPrefix": { + "isNot": "不是", + "startWith": "以...開頭", + "endWith": "以...結尾", + "isEmpty": "爲空", + "isNotEmpty": "不爲空" + } + }, + "checkboxFilter": { + "isChecked": "已核取", + "isUnchecked": "未核取", + "choicechipPrefix": { + "is": "是" + } + }, + "checklistFilter": { + "isComplete": "已完成", + "isIncomplted": "未完成" + }, + "singleSelectOptionFilter": { + "is": "是", + "isNot": "不是", + "isEmpty": "爲空", + "isNotEmpty": "不爲空" + }, + "multiSelectOptionFilter": { + "contains": "包含", + "doesNotContain": "不包含", + "isEmpty": "爲空", + "isNotEmpty": "不爲空" + }, + "field": { + "hide": "隱藏", + "insertLeft": "插入左方欄", + "insertRight": "插入右方欄", + "duplicate": "複製", + "delete": "刪除", + "textFieldName": "文字", + "checkboxFieldName": "核取方塊", + "dateFieldName": "日期", + "numberFieldName": "數字", + "singleSelectFieldName": "單選", + "multiSelectFieldName": "多選", + "urlFieldName": "網址", + "checklistFieldName": "核取列表", + "numberFormat": "數字格式", + "dateFormat": "日期格式", + "includeTime": "包含時間", + "dateFormatFriendly": "月 日, 年", + "dateFormatISO": "年-月-日", + "dateFormatLocal": "月/日/年", + "dateFormatUS": "年/月/日", + "timeFormat": "時間格式", + "invalidTimeFormat": "格式無效", + "timeFormatTwelveHour": "12 小時", + "timeFormatTwentyFourHour": "24 小時", + "addSelectOption": "新增選項", + "optionTitle": "選項", + "addOption": "新增選項", + "editProperty": "編輯內容", + "newProperty": "新欄位", + "deleteFieldPromptMessage": "你確定嗎?這個內容將被刪除" + }, + "sort": { + "ascending": "升冪排序", + "descending": "降冪排序", + "deleteSort": "刪除排序", + "addSort": "新增排序" + }, + "row": { + "duplicate": "複製", + "delete": "刪除", + "textPlaceholder": "空", + "copyProperty": "已將內容複製至剪貼簿", + "count": "Count", + "newRow": "新列表" + }, + "selectOption": { + "create": "建立", + "purpleColor": "紫色", + "pinkColor": "粉色", + "lightPinkColor": "淡粉色", + "orangeColor": "橘色", + "yellowColor": "黃色", + "limeColor": "萊姆色", + "greenColor": "綠色", + "aquaColor": "水藍色", + "blueColor": "藍色", + "deleteTag": "刪除標籤", + "colorPanelTitle": "顏色", + "panelTitle": "搜尋或建立選項", + "searchOption": "搜尋選項" + }, + "checklist": { + "panelTitle": "新增物件" + }, + "menuName": "網格" + }, + "document": { + "menuName": "檔案", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "選擇要連結的看板", + "createANewBoard": "建立新的看板" + }, + "grid": { + "selectAGridToLinkTo": "選擇要連結的網格", + "createANewGrid": "建立新網格" + } + }, + "plugins": { + "referencedBoard": "已參照的看板", + "referencedGrid": "已參照的網格", + "autoGeneratorMenuItemName": "OpenAI 寫手", + "autoGeneratorTitleName": "OpenAI: 叫人工智慧寫下任何事情...", + "autoGeneratorLearnMore": "Learn more", + "autoGeneratorGenerate": "產生", + "autoGeneratorHintText": "問 OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "無法取得 OpenAI 密鑰", + "smartEdit": "人工智慧助理", + "openAI": "OpenAI", + "smartEditFixSpelling": "修正拼寫", + "warning": "⚠️ AI 的回答可能不精確或是存在誤導", + "smartEditSummarize": "總結", + "smartEditCouldNotFetchResult": "無法取得 OpenAI 的結果", + "smartEditCouldNotFetchKey": "無法取得 OpenAI 密鑰", + "smartEditDisabled": "在設定連結 OpenAI ", + "discardResponse": "你確定放棄人工智慧的回覆?", + "cover": { + "changeCover": "更換封面", + "colors": "顏色", + "images": "圖片", + "abstract": "摘要", + "addCover": "新增封面", + "addLocalImage": "新增本機圖片", + "invalidImageUrl": "無效的圖片網址", + "failedToAddImageToGallery": "新增圖片到圖庫失敗", + "enterImageUrl": "輸入圖片網址", + "add": "Add", + "back": "Back", + "saveToGallery": "儲存到", + "removeIcon": "移除圖標", + "pasteImageUrl": "複製圖片網址", + "or": "或", + "pickFromFiles": "挑選檔案", + "couldNotFetchImage": "無法截取圖片", + "imageSavingFailed": "圖片儲存失敗", + "addIcon": "新增圖標" + } + } + }, + "board": { + "column": { + "create_new_card": "建立" + }, + "menuName": "看板" + }, + "calendar": { + "menuName": "日曆", + "defaultNewCalendarTitle": "未命名的", + "navigation": { + "today": "今天", + "jumpToday": "跳至今天", + "previousMonth": "上個月", + "nextMonth": "下個月" + }, + "settings": { + "showWeekNumbers": "顯示星期", + "showWeekends": "顯示週末", + "firstDayOfWeek": "一週的第一天", + "layoutDateField": "排列方式" + } + } +} \ No newline at end of file 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/cargokit_options.yaml b/frontend/appflowy_flutter/cargokit_options.yaml deleted file mode 100644 index 70d10df337..0000000000 --- a/frontend/appflowy_flutter/cargokit_options.yaml +++ /dev/null @@ -1 +0,0 @@ -use_precompiled_binaries: true 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/dev.env b/frontend/appflowy_flutter/dev.env deleted file mode 100644 index 6463736cc9..0000000000 --- a/frontend/appflowy_flutter/dev.env +++ /dev/null @@ -1 +0,0 @@ -APPFLOWY_CLOUD_URL= \ No newline at end of file diff --git a/frontend/appflowy_flutter/devtools_options.yaml b/frontend/appflowy_flutter/devtools_options.yaml deleted file mode 100644 index 7e7e7f67de..0000000000 --- a/frontend/appflowy_flutter/devtools_options.yaml +++ /dev/null @@ -1 +0,0 @@ -extensions: 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/desktop/uncategorized/board_test.dart b/frontend/appflowy_flutter/integration_test/board_test.dart similarity index 92% rename from frontend/appflowy_flutter/integration_test/desktop/uncategorized/board_test.dart rename to frontend/appflowy_flutter/integration_test/board_test.dart index 5e88b38697..3ac479bd70 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/board_test.dart +++ b/frontend/appflowy_flutter/integration_test/board_test.dart @@ -1,8 +1,7 @@ import 'package:appflowy_board/appflowy_board.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; +import 'util/util.dart'; /// Integration tests for an empty board. The [TestWorkspaceService] will load /// a workspace from an empty board `assets/test/workspaces/board.zip` for all @@ -33,8 +32,8 @@ void main() { const service = TestWorkspaceService(TestWorkspace.board); group('board', () { - setUpAll(() async => service.setUpAll()); - setUp(() async => service.setUp()); + setUpAll(() async => await service.setUpAll()); + setUp(() async => await service.setUp()); testWidgets('open the board with data structure in v0.2.0', (tester) async { await tester.initializeAppFlowy(); diff --git a/frontend/appflowy_flutter/integration_test/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart new file mode 100644 index 0000000000..c3ef83c267 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart @@ -0,0 +1,78 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('database', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('update calendar layout', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateCalendarButton(); + + // open setting + await tester.tapDatabaseSettingButton(); + await tester.tapDatabaseLayoutButton(); + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); + + await tester.tapDatabaseSettingButton(); + await tester.tapDatabaseLayoutButton(); + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid); + + await tester.pumpAndSettle(); + }); + + testWidgets('calendar start from day setting', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create calendar view + await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar'); + + // Open setting + await tester.tapDatabaseSettingButton(); + await tester.tapCalendarLayoutSettingButton(); + + // select the first day of week is Monday + await tester.tapFirstDayOfWeek(); + await tester.tapFirstDayOfWeekStartFromMonday(); + + // Open the other page and open the new calendar page again + await tester.openPage(readme); + await tester.pumpAndSettle(const Duration(milliseconds: 300)); + await tester.openPage('calendar'); + + // Open setting again and check the start from Monday is selected + await tester.tapDatabaseSettingButton(); + await tester.tapCalendarLayoutSettingButton(); + await tester.tapFirstDayOfWeek(); + tester.assertFirstDayOfWeekStartFromMonday(); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/database_cell_test.dart new file mode 100644 index 0000000000..a01ea6609f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_cell_test.dart @@ -0,0 +1,210 @@ +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 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid cell', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('edit text cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + await tester.editCell( + rowIndex: 0, + fieldType: FieldType.RichText, + input: 'hello world', + ); + + await tester.assertCellContent( + rowIndex: 0, + fieldType: FieldType.RichText, + content: 'hello world', + ); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit number cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.Number; + + // Create a number field + await tester.createField(fieldType, fieldType.name); + + await tester.editCell( + rowIndex: 0, + fieldType: fieldType, + input: '-1', + ); + // edit the next cell to force the previous cell at row 0 to lose focus + await tester.editCell( + rowIndex: 1, + fieldType: fieldType, + input: '0.2', + ); + // -1 -> -1 + await tester.assertCellContent( + rowIndex: 0, + fieldType: fieldType, + content: '-1', + ); + + // edit the next cell to force the previous cell at row 1 to lose focus + await tester.editCell( + rowIndex: 2, + fieldType: fieldType, + input: '.1', + ); + // 0.2 -> 0.2 + await tester.assertCellContent( + rowIndex: 1, + fieldType: fieldType, + content: '0.2', + ); + + // edit the next cell to force the previous cell at row 2 to lose focus + await tester.editCell( + rowIndex: 0, + fieldType: fieldType, + input: '', + ); + // .1 -> 0.1 + await tester.assertCellContent( + rowIndex: 2, + fieldType: fieldType, + content: '0.1', + ); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit checkbox cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + await tester.assertCheckboxCell(rowIndex: 0, isSelected: false); + await tester.tapCheckboxCellInGrid(rowIndex: 0); + await tester.assertCheckboxCell(rowIndex: 0, isSelected: true); + + await tester.tapCheckboxCellInGrid(rowIndex: 1); + await tester.tapCheckboxCellInGrid(rowIndex: 2); + await tester.assertCheckboxCell(rowIndex: 1, isSelected: true); + await tester.assertCheckboxCell(rowIndex: 2, isSelected: true); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit create time cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.CreatedTime; + // Create a create time field + // The create time field is not editable + await tester.createField(fieldType, fieldType.name); + + await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); + + await tester.findDateEditor(findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit last time cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.LastEditedTime; + // Create a last time field + // The last time field is not editable + await tester.createField(fieldType, fieldType.name); + + await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); + + await tester.findDateEditor(findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit time cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.DateTime; + await tester.createField(fieldType, fieldType.name); + + // Tap the cell to invoke the field editor + await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); + await tester.findDateEditor(findsOneWidget); + + // Select the date + await tester.selectDay(content: 3); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit single select cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + const fieldType = FieldType.SingleSelect; + await tester.tapAddButton(); + // When create a grid, it will create a single select field by default + await tester.tapCreateGridButton(); + + // Tap the cell to invoke the selection option editor + await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); + await tester.findSelectOptionEditor(findsOneWidget); + + await tester.createOption(name: 'hello world'); + await tester.dismissSelectOptionEditor(); + + // Make sure the option is created and displayed in the cell + await tester.findSelectOptionWithNameInGrid( + rowIndex: 0, + name: 'hello world', + ); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_field_test.dart b/frontend/appflowy_flutter/integration_test/database_field_test.dart new file mode 100644 index 0000000000..ec4f1f4d35 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_field_test.dart @@ -0,0 +1,201 @@ +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.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 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid page', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('rename existing field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Invoke the field editor + await tester.tapGridFieldWithName('Name'); + await tester.tapEditPropertyButton(); + + await tester.renameField('hello world'); + await tester.dismissFieldEditor(); + + await tester.tapGridFieldWithName('hello world'); + await tester.pumpAndSettle(); + }); + + testWidgets('update field type of existing field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Invoke the field editor + await tester.tapGridFieldWithName('Type'); + await tester.tapEditPropertyButton(); + + await tester.tapTypeOptionButton(); + await tester.selectFieldType(FieldType.Checkbox); + await tester.dismissFieldEditor(); + + await tester.assertFieldTypeWithFieldName( + 'Type', + FieldType.Checkbox, + ); + await tester.pumpAndSettle(); + }); + + testWidgets('create a field and rename it', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.createField(FieldType.Checklist, 'checklist'); + + // check the field is created successfully + await tester.findFieldWithName('checklist'); + await tester.pumpAndSettle(); + }); + + testWidgets('delete field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.createField(FieldType.Checkbox, 'New field 1'); + + // Delete the field + await tester.tapGridFieldWithName('New field 1'); + await tester.tapDeletePropertyButton(); + + // confirm delete + await tester.tapDialogOkButton(); + + await tester.noFieldWithName('New field 1'); + await tester.pumpAndSettle(); + }); + + testWidgets('duplicate field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField('New field 1'); + await tester.dismissFieldEditor(); + + // Delete the field + await tester.tapGridFieldWithName('New field 1'); + await tester.tapDuplicatePropertyButton(); + + await tester.findFieldWithName('New field 1 (copy)'); + await tester.pumpAndSettle(); + }); + + testWidgets('hide field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField('New field 1'); + await tester.dismissFieldEditor(); + + // Delete the field + await tester.tapGridFieldWithName('New field 1'); + await tester.tapHidePropertyButton(); + + await tester.noFieldWithName('New field 1'); + await tester.pumpAndSettle(); + }); + + testWidgets('create checklist field ', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + + // Open the type option menu + await tester.tapTypeOptionButton(); + + 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.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + for (final fieldType in [ + FieldType.Checklist, + FieldType.DateTime, + FieldType.Number, + FieldType.URL, + FieldType.MultiSelect, + FieldType.LastEditedTime, + FieldType.CreatedTime, + FieldType.Checkbox, + ]) { + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField(fieldType.name); + + // Open the type option menu + await tester.tapTypeOptionButton(); + + await tester.selectFieldType(fieldType); + await tester.dismissFieldEditor(); + + // After update the field type, the cells should be updated + await tester.findCellByFieldType(fieldType); + await tester.pumpAndSettle(); + } + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_filter_test.dart b/frontend/appflowy_flutter/integration_test/database_filter_test.dart new file mode 100644 index 0000000000..de509a8b84 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_filter_test.dart @@ -0,0 +1,158 @@ +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.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 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid', () { + const location = 'import_files'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('add text filter', (tester) async { + await tester.openV020database(); + + // create a filter + await tester.tapDatabaseFilterButton(); + await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name'); + await tester.tapFilterButtonInGrid('Name'); + + // enter 'A' in the filter text field + await tester.assertNumberOfRowsInGridPage(10); + await tester.enterTextInTextFilter('A'); + await tester.assertNumberOfRowsInGridPage(1); + + // after remove the filter, the grid should show all rows + await tester.enterTextInTextFilter(''); + await tester.assertNumberOfRowsInGridPage(10); + + await tester.enterTextInTextFilter('B'); + await tester.assertNumberOfRowsInGridPage(1); + + // open the menu to delete the filter + await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor)); + await tester.tapDeleteFilterButtonInGrid(); + await tester.assertNumberOfRowsInGridPage(10); + + await tester.pumpAndSettle(); + }); + + testWidgets('add checkbox filter', (tester) async { + await tester.openV020database(); + + // create a filter + await tester.tapDatabaseFilterButton(); + await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done'); + await tester.assertNumberOfRowsInGridPage(5); + + await tester.tapFilterButtonInGrid('Done'); + await tester.tapCheckboxFilterButtonInGrid(); + + await tester.tapUnCheckedButtonOnCheckboxFilter(); + await tester.assertNumberOfRowsInGridPage(5); + + await tester + .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); + await tester.tapDeleteFilterButtonInGrid(); + await tester.assertNumberOfRowsInGridPage(10); + + await tester.pumpAndSettle(); + }); + + testWidgets('add checklist filter', (tester) async { + await tester.openV020database(); + + // create a filter + await tester.tapDatabaseFilterButton(); + await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist'); + + // By default, the condition of checklist filter is 'uncompleted' + await tester.assertNumberOfRowsInGridPage(9); + + await tester.tapFilterButtonInGrid('checklist'); + await tester.tapChecklistFilterButtonInGrid(); + + await tester.tapCompletedButtonOnChecklistFilter(); + await tester.assertNumberOfRowsInGridPage(1); + + await tester.pumpAndSettle(); + }); + + testWidgets('add single select filter', (tester) async { + await tester.openV020database(); + + // create a filter + await tester.tapDatabaseFilterButton(); + await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type'); + + await tester.tapFilterButtonInGrid('Type'); + + // select the option 's6' + await tester.tapOptionFilterWithName('s6'); + await tester.assertNumberOfRowsInGridPage(0); + + // unselect the option 's6' + await tester.tapOptionFilterWithName('s6'); + await tester.assertNumberOfRowsInGridPage(10); + + // select the option 's5' + await tester.tapOptionFilterWithName('s5'); + await tester.assertNumberOfRowsInGridPage(1); + + // select the option 's4' + await tester.tapOptionFilterWithName('s4'); + + // The row with 's4' or 's5' should be shown. + await tester.assertNumberOfRowsInGridPage(2); + + await tester.pumpAndSettle(); + }); + + testWidgets('add multi select filter', (tester) async { + await tester.openV020database(); + + // create a filter + await tester.tapDatabaseFilterButton(); + await tester.tapCreateFilterByFieldType( + FieldType.MultiSelect, + 'multi-select', + ); + + await tester.tapFilterButtonInGrid('multi-select'); + await tester.scrollOptionFilterListByOffset(const Offset(0, -200)); + + // select the option 'm1'. Any option with 'm1' should be shown. + await tester.tapOptionFilterWithName('m1'); + await tester.assertNumberOfRowsInGridPage(5); + await tester.tapOptionFilterWithName('m1'); + + // select the option 'm2'. Any option with 'm2' should be shown. + await tester.tapOptionFilterWithName('m2'); + await tester.assertNumberOfRowsInGridPage(4); + await tester.tapOptionFilterWithName('m2'); + + // select the option 'm4'. Any option with 'm4' should be shown. + await tester.tapOptionFilterWithName('m4'); + await tester.assertNumberOfRowsInGridPage(1); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart new file mode 100644 index 0000000000..10090121b8 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart @@ -0,0 +1,229 @@ +import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/ime.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('insert emoji in the row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + await tester.hoverRowBanner(); + + await tester.openEmojiPicker(); + await tester.switchToEmojiList(); + await tester.tapEmoji('😀'); + + // After select the emoji, the EmojiButton will show up + await tester.tapButton(find.byType(EmojiButton)); + }); + + testWidgets('update emoji in the row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + await tester.hoverRowBanner(); + await tester.openEmojiPicker(); + await tester.switchToEmojiList(); + await tester.tapEmoji('😀'); + + // Update existing selected emoji + await tester.tapButton(find.byType(EmojiButton)); + await tester.switchToEmojiList(); + await tester.tapEmoji('😅'); + + // The emoji already displayed in the row banner + final emojiText = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == '😅', + ); + + // 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)); + }); + + testWidgets('remove emoji in the row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + await tester.hoverRowBanner(); + await tester.openEmojiPicker(); + await tester.switchToEmojiList(); + await tester.tapEmoji('😀'); + + // Remove the emoji + await tester.tapButton(find.byType(RemoveEmojiButton)); + final emojiText = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == '😀', + ); + expect(emojiText, findsNothing); + }); + + testWidgets('create list of fields in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + for (final fieldType in [ + FieldType.Checklist, + FieldType.DateTime, + FieldType.Number, + FieldType.URL, + FieldType.MultiSelect, + FieldType.LastEditedTime, + FieldType.CreatedTime, + FieldType.Checkbox, + ]) { + await tester.tapRowDetailPageCreatePropertyButton(); + await tester.renameField(fieldType.name); + + // Open the type option menu + await tester.tapTypeOptionButton(); + + await tester.selectFieldType(fieldType); + await tester.dismissFieldEditor(); + + // After update the field type, the cells should be updated + await tester.findCellByFieldType(fieldType); + await tester.scrollRowDetailByOffset(const Offset(0, -50)); + } + }); + + testWidgets('check document is exist in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + // Each row detail page should have a document + await tester.assertDocumentExistInRowDetailPage(); + }); + + testWidgets('update the content of the document and re-open it', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + // Wait for the document to be loaded + await tester.wait(500); + + // Focus on the editor + final textBlock = find.byType(TextBlockComponentWidget); + await tester.tapAt(tester.getCenter(textBlock)); + + // Input some text + const inputText = 'Hello world'; + await tester.ime.insertText(inputText); + expect( + find.textContaining(inputText, findRichText: true), + findsOneWidget, + ); + + // Tap outside to dismiss the field + await tester.tapAt(Offset.zero); + await tester.pumpAndSettle(); + + // Re-open the document + await tester.openFirstRowDetailPage(); + expect( + find.textContaining(inputText, findRichText: true), + findsOneWidget, + ); + }); + + testWidgets('delete row in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + await tester.tapRowDetailPageDeleteRowButton(); + await tester.tapEscButton(); + + await tester.assertNumberOfRowsInGridPage(2); + }); + + testWidgets('duplicate row in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + await tester.tapRowDetailPageDuplicateRowButton(); + await tester.tapEscButton(); + + await tester.assertNumberOfRowsInGridPage(4); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_row_test.dart b/frontend/appflowy_flutter/integration_test/database_row_test.dart new file mode 100644 index 0000000000..14a336a89f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_row_test.dart @@ -0,0 +1,85 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('create row of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.tapCreateRowButtonInGrid(); + + // The initial number of rows is 3 + await tester.assertNumberOfRowsInGridPage(4); + await tester.pumpAndSettle(); + }); + + testWidgets('create row from row menu of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.hoverOnFirstRowOfGrid(); + + await tester.tapCreateRowButtonInRowMenuOfGrid(); + + // The initial number of rows is 3 + await tester.assertNumberOfRowsInGridPage(4); + await tester.assertRowCountInGridPage(4); + await tester.pumpAndSettle(); + }); + + testWidgets('delete row of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.hoverOnFirstRowOfGrid(); + + // Open the row menu and then click the delete + await tester.tapRowMenuButtonInGrid(); + await tester.tapDeleteOnRowMenu(); + + // The initial number of rows is 3 + await tester.assertNumberOfRowsInGridPage(2); + await tester.assertRowCountInGridPage(2); + await tester.pumpAndSettle(); + }); + + testWidgets('check number of row indicator in the initial grid', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.assertRowCountInGridPage(3); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_setting_test.dart b/frontend/appflowy_flutter/integration_test/database_setting_test.dart new file mode 100644 index 0000000000..ba4e88ea9f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_setting_test.dart @@ -0,0 +1,66 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('update layout', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // open setting + await tester.tapDatabaseSettingButton(); + // select the layout + await tester.tapDatabaseLayoutButton(); + // select layout by board + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); + + await tester.pumpAndSettle(); + }); + + testWidgets('update layout multiple times', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // open setting + await tester.tapDatabaseSettingButton(); + await tester.tapDatabaseLayoutButton(); + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); + + await tester.tapDatabaseSettingButton(); + await tester.tapDatabaseLayoutButton(); + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Calendar); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_share_test.dart b/frontend/appflowy_flutter/integration_test/database_share_test.dart new file mode 100644 index 0000000000..15a3a33548 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_share_test.dart @@ -0,0 +1,215 @@ +import 'dart:io'; + +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/mock/mock_file_picker.dart'; +import 'util/util.dart'; +import 'package:path/path.dart' as p; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('database', () { + const location = 'import_files'; + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('import v0.2.0 database data', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // expect to see a readme page + tester.expectToSeePageName(readme); + + await tester.tapAddButton(); + await tester.tapImportButton(); + + final testFileNames = ['v020.afdb']; + final fileLocation = await tester.currentFileLocation(); + for (final fileName in testFileNames) { + final str = await rootBundle.loadString( + p.join( + 'assets/test/workspaces/database', + fileName, + ), + ); + File(p.join(fileLocation, fileName)).writeAsStringSync(str); + } + // mock get files + await mockPickFilePaths(testFileNames, name: location); + await tester.tapDatabaseRawDataButton(); + await tester.openPage('v020'); + + // check the import content + // await tester.assertCellContent( + // rowIndex: 7, + // fieldType: FieldType.RichText, + // // fieldName: 'Name', + // content: '', + // ); + + // check the text cell + final textCells = ['A', 'B', 'C', 'D', 'E', '', '', '', '', '']; + for (final (index, content) in textCells.indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.RichText, + content: content, + ); + } + + // check the checkbox cell + final checkboxCells = [ + true, + true, + true, + true, + true, + false, + false, + false, + false, + false + ]; + for (final (index, content) in checkboxCells.indexed) { + await tester.assertCheckboxCell( + rowIndex: index, + isSelected: content, + ); + } + + // check the number cell + final numberCells = [ + '-1', + '-2', + '0.1', + '0.2', + '1', + '2', + '10', + '11', + '12', + '' + ]; + for (final (index, content) in numberCells.indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.Number, + content: content, + ); + } + + // check the url cell + final urlCells = [ + 'appflowy.io', + 'no url', + 'appflowy.io', + 'https://github.com/AppFlowy-IO/', + '', + '', + ]; + for (final (index, content) in urlCells.indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.URL, + content: content, + ); + } + + // check the single select cell + final singleSelectCells = [ + 's1', + 's2', + 's3', + 's4', + 's5', + '', + '', + '', + '', + '', + ]; + for (final (index, content) in singleSelectCells.indexed) { + await tester.assertSingleSelectOption( + rowIndex: index, + content: content, + ); + } + + // check the multi select cell + final List> multiSelectCells = [ + ['m1'], + ['m1', 'm2'], + ['m1', 'm2', 'm3'], + ['m1', 'm2', 'm3'], + ['m1', 'm2', 'm3', 'm4', 'm5'], + [], + [], + [], + [], + [], + ]; + for (final (index, contents) in multiSelectCells.indexed) { + await tester.assertMultiSelectOption( + rowIndex: index, + contents: contents, + ); + } + + // check the checklist cell + final List checklistCells = [ + 0.6, + 0.3, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ]; + for (final (index, percent) in checklistCells.indexed) { + await tester.assertChecklistCellInGrid( + rowIndex: index, + percent: percent, + ); + } + + // check the date cell + final List dateCells = [ + 'Jun 01, 2023', + 'Jun 02, 2023', + 'Jun 03, 2023', + 'Jun 04, 2023', + 'Jun 05, 2023', + 'Jun 05, 2023', + 'Jun 16, 2023', + '', + '', + '' + ]; + for (final (index, content) in dateCells.indexed) { + await tester.assertDateCellInGrid( + rowIndex: index, + fieldType: FieldType.DateTime, + content: content, + ); + } + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_sort_test.dart b/frontend/appflowy_flutter/integration_test/database_sort_test.dart new file mode 100644 index 0000000000..26272be0ab --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_sort_test.dart @@ -0,0 +1,283 @@ +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 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid', () { + const location = 'import_files'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('add text sort', (tester) async { + await tester.openV020database(); + // create a filter + await tester.tapDatabaseSortButton(); + await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); + + // check the text cell order + final textCells = [ + '', + '', + '', + '', + '', + 'A', + 'B', + 'C', + 'D', + 'E', + ]; + for (final (index, content) in textCells.indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.RichText, + content: content, + ); + } + + // open the sort menu and select order by descending + await tester.tapSortMenuInSettingBar(); + await tester.tapSortButtonByName('Name'); + await tester.tapSortByDescending(); + for (final (index, content) in [ + 'E', + 'D', + 'C', + 'B', + 'A', + '', + '', + '', + '', + '', + ].indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.RichText, + content: content, + ); + } + + // delete all sorts + await tester.tapSortMenuInSettingBar(); + await tester.tapAllSortButton(); + + // check the text cell order + for (final (index, content) in [ + 'A', + 'B', + 'C', + 'D', + 'E', + '', + '', + '', + '', + '', + ].indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.RichText, + content: content, + ); + } + await tester.pumpAndSettle(); + }); + + testWidgets('add checkbox sort', (tester) async { + await tester.openV020database(); + // create a filter + await tester.tapDatabaseSortButton(); + await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); + + // check the checkbox cell order + for (final (index, content) in [ + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + ].indexed) { + await tester.assertCheckboxCell( + rowIndex: index, + isSelected: content, + ); + } + + // open the sort menu and select order by descending + await tester.tapSortMenuInSettingBar(); + await tester.tapSortButtonByName('Done'); + await tester.tapSortByDescending(); + for (final (index, content) in [ + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + ].indexed) { + await tester.assertCheckboxCell( + rowIndex: index, + isSelected: content, + ); + } + + await tester.pumpAndSettle(); + }); + + testWidgets('add number sort', (tester) async { + await tester.openV020database(); + // create a filter + 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) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.Number, + content: content, + ); + } + + // open the sort menu and select order by descending + await tester.tapSortMenuInSettingBar(); + await tester.tapSortButtonByName('number'); + await tester.tapSortByDescending(); + for (final (index, content) in [ + '12', + '11', + '10', + '2', + '1', + '0.2', + '0.1', + '-1', + '-2', + '', + ].indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.Number, + content: content, + ); + } + + await tester.pumpAndSettle(); + }); + + testWidgets('add number and text sort', (tester) async { + await tester.openV020database(); + // create a filter + await tester.tapDatabaseSortButton(); + await tester.tapCreateSortByFieldType(FieldType.Number, 'number'); + + // open the sort menu and select number order by descending + await tester.tapSortMenuInSettingBar(); + await tester.tapSortButtonByName('number'); + await tester.tapSortByDescending(); + for (final (index, content) in [ + '12', + '11', + '10', + '2', + '1', + '0.2', + '0.1', + '-1', + '-2', + '', + ].indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.Number, + content: content, + ); + } + + await tester.tapSortMenuInSettingBar(); + await tester.tapCreateSortByFieldTypeInSortMenu( + FieldType.RichText, + 'Name', + ); + + // check number cell order + for (final (index, content) in [ + '12', + '11', + '10', + '2', + '', + '-1', + '-2', + '0.1', + '0.2', + '1', + ].indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.Number, + content: content, + ); + } + + // check text cell order + for (final (index, content) in [ + '', + '', + '', + '', + '', + 'A', + 'B', + 'C', + 'D', + 'E', + ].indexed) { + await tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.RichText, + content: content, + ); + } + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_view_test.dart b/frontend/appflowy_flutter/integration_test/database_view_test.dart new file mode 100644 index 0000000000..7d787f2fef --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_view_test.dart @@ -0,0 +1,96 @@ +import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('database', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('create linked view', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Create board view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); + + // Create grid view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid); + + // Create calendar view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar); + + await tester.pumpAndSettle(); + }); + + testWidgets('rename and delete linked view', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Create board view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); + + // rename board view + await tester.renameLinkedView( + tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board), + 'new board', + ); + final findBoard = tester.findTabBarLinkViewByViewName('new board'); + expect(findBoard, findsOneWidget); + + // delete the board + await tester.deleteDatebaseView(findBoard); + expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('delete the last database view', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Create board view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); + + // delete the board + await tester.deleteDatebaseView( + tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board), + ); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_add_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_add_row_test.dart deleted file mode 100644 index 84db6a5be0..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_add_row_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.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/widgets/card/card.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_board/appflowy_board.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -const defaultFirstCardName = 'Card 1'; -const defaultLastCardName = 'Card 3'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('board add row test:', () { - testWidgets('from header', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - - final firstCard = find.byType(RowCard).first; - - expect( - find.descendant( - of: firstCard, - matching: find.text(defaultFirstCardName), - ), - findsOneWidget, - ); - - await tester.tap( - find - .descendant( - of: find.byType(BoardColumnHeader), - matching: find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s, - ), - ) - .at(1), - ); - await tester.pumpAndSettle(); - - const newCardName = 'Card 4'; - await tester.enterText( - find.descendant( - of: firstCard, - matching: find.byType(TextField), - ), - newCardName, - ); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - - await tester.tap(find.byType(AppFlowyBoard)); - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(RowCard).first, - matching: find.text(newCardName), - ), - findsOneWidget, - ); - }); - - testWidgets('from footer', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - - final lastCard = find.byType(RowCard).last; - - expect( - find.descendant( - of: lastCard, - matching: find.text(defaultLastCardName), - ), - findsOneWidget, - ); - - await tester.tapButton( - find.byType(BoardColumnFooter).at(1), - ); - - const newCardName = 'Card 4'; - await tester.enterText( - find.descendant( - of: find.byType(BoardColumnFooter), - matching: find.byType(TextField), - ), - newCardName, - ); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - - await tester.tap(find.byType(AppFlowyBoard)); - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(RowCard).last, - matching: find.text(newCardName), - ), - findsOneWidget, - ); - }); - }); -} 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 deleted file mode 100644 index bdd0ecdb2c..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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/database_test_op.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('board field test', () { - testWidgets('change field type whithin card #5360', (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.tapButton(card1); - - const fieldName = "test change field"; - await tester.createField( - FieldType.RichText, - name: fieldName, - layout: ViewLayoutPB.Board, - ); - await tester.dismissRowDetailPage(); - await tester.tapButton(card1); - await tester.changeFieldTypeOfFieldWithName( - fieldName, - FieldType.Checkbox, - layout: ViewLayoutPB.Board, - ); - await tester.hoverOnWidget(find.text('Card 2')); - }); - }); -} 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 deleted file mode 100644 index 3eedbdb3bf..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -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_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:', () { - testWidgets('move row to another group', (tester) async { - const card1Name = 'Card 1'; - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - final card1 = find.ancestor( - of: find.text(card1Name), - matching: find.byType(RowCard), - ); - final doingGroup = find.text('Doing'); - final doingGroupCenter = tester.getCenter(doingGroup); - final card1Center = tester.getCenter(card1); - - await tester.timedDrag( - card1, - doingGroupCenter.translate(-card1Center.dx, -card1Center.dy), - const Duration(seconds: 1), - ); - await tester.pumpAndSettle(); - await tester.tap(card1); - await tester.pumpAndSettle(); - - final card1StatusFinder = find.descendant( - of: find.byType(RowPropertyList), - matching: find.descendant( - of: find.byType(SelectOptionTag), - matching: find.byType(Text), - ), - ); - expect(card1StatusFinder, findsNWidgets(1)); - 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 deleted file mode 100644 index 6a012ac763..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -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/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('board group options:', () { - testWidgets('expand/collapse hidden groups', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - - final collapseFinder = find.byFlowySvg(FlowySvgs.pull_left_outlined_s); - 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 - await tester.tap(collapseFinder); - await tester.pumpAndSettle(); - - // Is expanded - expect(collapseFinder, findsNothing); - expect(expandFinder, findsOneWidget); - }); - - testWidgets('hide first group, and show it again', (tester) async { - await tester.initializeAppFlowy(); - 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( - of: find.byType(BoardColumnHeader), - matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), - ) - .first; - - await tester.tap(optionsFinder); - await tester.pumpAndSettle(); - - // Tap the hide option - await tester.tap(find.byFlowySvg(FlowySvgs.hide_s)); - await tester.pumpAndSettle(); - - int shownGroups = - tester.widgetList(find.byType(BoardColumnHeader)).length; - - // We still show Doing, Done, No Status - expect(shownGroups, 3); - - final hiddenCardFinder = find.byType(HiddenGroupCard); - await tester.hoverOnWidget(hiddenCardFinder); - await tester.tap(find.byFlowySvg(FlowySvgs.show_m)); - await tester.pumpAndSettle(); - - 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); - - 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); - - // 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 the delete button and confirm - await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s)); - await tester.tapButtonWithName(LocaleKeys.space_delete.tr()); - - // Expect number of groups to decrease by one - expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3); - }); - }); -} 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 deleted file mode 100644 index 868c27d302..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/card/card.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_board/appflowy_board.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:time/time.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; - -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(); - - 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_delete.tr()); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - expect(find.text(name), findsNothing); - }); - - testWidgets('duplicate 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.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)); - }); - - 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(); - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - - // assert number of groups - tester.assertNumberOfGroups(4); - - // scroll the board horizontally to ensure add new group button appears - await tester.scrollBoardToEnd(); - - // assert and click on add new group button - tester.assertNewGroupTextField(false); - await tester.tapNewGroupButton(); - tester.assertNewGroupTextField(true); - - // enter new group name and submit - await tester.enterNewGroupName('needs design', submit: true); - - // assert number of groups has increased - tester.assertNumberOfGroups(5); - - // assert text field has disappeared - await tester.scrollBoardToEnd(); - tester.assertNewGroupTextField(false); - - // click on add new group button - await tester.tapNewGroupButton(); - tester.assertNewGroupTextField(true); - - // type some things - await tester.enterNewGroupName('needs planning', submit: false); - - // click on clear button and assert empty contents - await tester.clearNewGroupTextField(); - - // press escape to cancel - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - tester.assertNewGroupTextField(false); - - // click on add new group button - await tester.tapNewGroupButton(); - tester.assertNewGroupTextField(true); - - // press elsewhere to cancel - await tester.tap(find.byType(AppFlowyBoard)); - await tester.pumpAndSettle(); - tester.assertNewGroupTextField(false); - }); - }); -} 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 deleted file mode 100644 index 75323a1c80..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart +++ /dev/null @@ -1,18 +0,0 @@ -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(); - - // Board integration tests - 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/change_name_and_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart deleted file mode 100644 index f205b35354..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart +++ /dev/null @@ -1,75 +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/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.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 '../../../shared/util.dart'; -import '../../../shared/workspace.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - const icon = '😄'; - const name = 'AppFlowy'; - final email = '${uuid()}@appflowy.io'; - - testWidgets('change name and icon', (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, // use the same email to check the next test - ); - - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - var workspaceIcon = tester.widget( - find.byType(WorkspaceIcon), - ); - expect(workspaceIcon.workspace.icon, ''); - - await tester.openWorkspaceMenu(); - await tester.changeWorkspaceIcon(icon); - await tester.changeWorkspaceName(name); - - await tester.pumpUntilNotFound( - find.text(LocaleKeys.workspace_renameSuccess.tr()), - ); - - workspaceIcon = tester.widget( - find.byType(WorkspaceIcon), - ); - expect(workspaceIcon.workspace.icon, icon); - expect(find.findTextInFlowyText(name), findsOneWidget); - }); - - testWidgets('verify the result again after relaunching', (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, // use the same email to check the next test - ); - - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // check the result again - final workspaceIcon = tester.widget( - find.byType(WorkspaceIcon), - ); - expect(workspaceIcon.workspace.icon, icon); - expect(workspaceIcon.workspace.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/command_palette_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test.dart deleted file mode 100644 index 7bf9a86966..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Command Palette', () { - testWidgets('Toggle command palette', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.toggleCommandPalette(); - expect(find.byType(CommandPaletteModal), findsOneWidget); - - await tester.toggleCommandPalette(); - expect(find.byType(CommandPaletteModal), findsNothing); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test_runner.dart deleted file mode 100644 index b1e990361a..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test_runner.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'command_palette_test.dart' as command_palette_test; -import 'folder_search_test.dart' as folder_search_test; -import 'recent_history_test.dart' as recent_history_test; - -void startTesting() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Command Palette integration tests - command_palette_test.main(); - folder_search_test.main(); - recent_history_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 deleted file mode 100644 index 0b77a0167b..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - }); - - group('Folder Search', () { - testWidgets('Search for views', (tester) async { - const firstDocument = "ViewOne"; - const secondDocument = "ViewOna"; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(name: firstDocument); - await tester.createNewPageWithNameUnderParent(name: secondDocument); - - await tester.toggleCommandPalette(); - expect(find.byType(CommandPaletteModal), findsOneWidget); - - final searchFieldFinder = find.descendant( - of: find.byType(SearchField), - matching: find.byType(FlowyTextField), - ); - - await tester.enterText(searchFieldFinder, secondDocument); - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - // Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna) - expect(find.byType(SearchResultCell), 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); - - // 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, - ); - }); - }); - }); -} 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 deleted file mode 100644 index b9495ae0e7..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -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_views_list.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Recent History', () { - testWidgets('Search for views', (tester) async { - const firstDocument = "First"; - const secondDocument = "Second"; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(name: firstDocument); - await tester.createNewPageWithNameUnderParent(name: secondDocument); - - await tester.toggleCommandPalette(); - expect(find.byType(CommandPaletteModal), findsOneWidget); - - // Expect history list - expect(find.byType(RecentViewsList), findsOneWidget); - - // Expect three recent history items - expect(find.byType(SearchRecentViewCell), findsNWidgets(3)); - - // Expect the first item to be the last viewed document - final firstDocumentWidget = - tester.widget(find.byType(SearchRecentViewCell).first) - as SearchRecentViewCell; - 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 deleted file mode 100644 index 3a565cbee9..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart +++ /dev/null @@ -1,358 +0,0 @@ -import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -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() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('calendar', () { - testWidgets('update calendar layout', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - layout: ViewLayoutPB.Calendar, - ); - - // open setting - await tester.tapDatabaseSettingButton(); - await tester.tapDatabaseLayoutButton(); - await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); - await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); - - await tester.tapDatabaseSettingButton(); - await tester.tapDatabaseLayoutButton(); - await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid); - await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid); - - await tester.pumpAndSettle(); - }); - - testWidgets('calendar start from day setting', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // Create calendar view - const name = 'calendar'; - await tester.createNewPageWithNameUnderParent( - name: name, - layout: ViewLayoutPB.Calendar, - ); - - // Open setting - await tester.tapDatabaseSettingButton(); - await tester.tapCalendarLayoutSettingButton(); - - // select the first day of week is Monday - await tester.tapFirstDayOfWeek(); - await tester.tapFirstDayOfWeekStartFromMonday(); - - // Open the other page and open the new calendar page again - await tester.openPage(gettingStarted); - await tester.pumpAndSettle(const Duration(milliseconds: 300)); - await tester.openPage(name, layout: ViewLayoutPB.Calendar); - - // Open setting again and check the start from Monday is selected - await tester.tapDatabaseSettingButton(); - await tester.tapCalendarLayoutSettingButton(); - await tester.tapFirstDayOfWeek(); - tester.assertFirstDayOfWeekStartFromMonday(); - - await tester.pumpAndSettle(); - }); - - testWidgets('creating and editing calendar events', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // Create the calendar view - await tester.createNewPageWithNameUnderParent( - layout: ViewLayoutPB.Calendar, - ); - - // Scroll until today's date cell is visible - await tester.scrollToToday(); - - // Hover over today's calendar cell - await tester.hoverOnTodayCalendarCell( - // Tap on create new event button - onHover: tester.tapAddCalendarEventButton, - ); - - // Make sure that the event editor popup is shown - tester.assertEventEditorOpen(); - - tester.assertNumberOfEventsInCalendar(1); - - // Dismiss the event editor popup - await tester.dismissEventEditor(); - - // Double click on today's calendar cell to create a new event - await tester.doubleClickCalendarCell(DateTime.now()); - - // Make sure that the event is inserted in the cell - tester.assertNumberOfEventsInCalendar(2); - - // Click on the event - await tester.openCalendarEvent(index: 0); - tester.assertEventEditorOpen(); - - // Change the title of the event - await tester.editEventTitle('hello world'); - await tester.dismissEventEditor(); - - // Make sure that the event is edited - tester.assertNumberOfEventsInCalendar(1, title: 'hello world'); - tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now()); - - // Click on the event - await tester.openCalendarEvent(index: 0); - tester.assertEventEditorOpen(); - - // Click on the open icon - await tester.openEventToRowDetailPage(); - tester.assertRowDetailPageOpened(); - - // Duplicate the event - await tester.tapRowDetailPageRowActionButton(); - await tester.tapRowDetailPageDuplicateRowButton(); - await tester.dismissRowDetailPage(); - - // Check that there are 2 events - tester.assertNumberOfEventsInCalendar(2, title: 'hello world'); - tester.assertNumberOfEventsOnSpecificDay(3, DateTime.now()); - - // Delete an event - await tester.openCalendarEvent(index: 1); - await tester.deleteEventFromEventEditor(); - - // Check that there is 1 event - tester.assertNumberOfEventsInCalendar(1, title: 'hello world'); - tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now()); - - // Delete event from row detail page - await tester.openCalendarEvent(index: 0); - await tester.openEventToRowDetailPage(); - tester.assertRowDetailPageOpened(); - - await tester.tapRowDetailPageRowActionButton(); - await tester.tapRowDetailPageDeleteRowButton(); - - // Check that there is 0 event - tester.assertNumberOfEventsInCalendar(0, title: 'hello world'); - tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now()); - }); - - testWidgets('create and duplicate calendar event', (tester) async { - const customTitle = "EventTitleCustom"; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // Create the calendar view - await tester.createNewPageWithNameUnderParent( - layout: ViewLayoutPB.Calendar, - ); - - // Scroll until today's date cell is visible - await tester.scrollToToday(); - - // Hover over today's calendar cell - await tester.hoverOnTodayCalendarCell( - // Tap on create new event button - onHover: () async => tester.tapAddCalendarEventButton(), - ); - - // Make sure that the event editor popup is shown - tester.assertEventEditorOpen(); - - tester.assertNumberOfEventsInCalendar(1); - - // Change the title of the event - await tester.editEventTitle(customTitle); - - // Duplicate event - final duplicateBtnFinder = find - .descendant( - of: find.byType(CalendarEventEditor), - matching: find.byType( - FlowyIconButton, - ), - ) - .first; - await tester.tap(duplicateBtnFinder); - await tester.pumpAndSettle(); - - tester.assertNumberOfEventsInCalendar(2, title: customTitle); - }); - - testWidgets('rescheduling 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(); - - // Drag and drop the event onto the next week, same day - await tester.dragDropRescheduleCalendarEvent(); - - // Make sure that the event has been rescheduled to the new date - final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7)); - tester.assertNumberOfEventsInCalendar(1); - tester.assertNumberOfEventsOnSpecificDay(1, sameDayNextWeek); - - // Delete the event - await tester.openCalendarEvent(index: 0, date: sameDayNextWeek); - await tester.deleteEventFromEventEditor(); - - // Create another event on the 5th of this month - final fifthOfThisMonth = DateTime(today.year, today.month, 5); - await tester.doubleClickCalendarCell(fifthOfThisMonth); - await tester.dismissEventEditor(); - - // Make sure that the event is on the 4t - tester.assertNumberOfEventsOnSpecificDay(1, fifthOfThisMonth); - - // Click on the event - await tester.openCalendarEvent(index: 0, date: fifthOfThisMonth); - - // Open the date editor of the event - await tester.tapDateCellInRowDetailPage(); - await tester.findDateEditor(findsOneWidget); - - // Edit the event's date - final newDate = fifthOfThisMonth.add(const Duration(days: 1)); - await tester.selectDay(content: newDate.day); - await tester.dismissCellEditor(); - - // Dismiss the event editor - await tester.dismissEventEditor(); - - // Make sure that the event is edited - tester.assertNumberOfEventsInCalendar(1); - tester.assertNumberOfEventsOnSpecificDay(1, newDate); - - // Click on the unscheduled events button - await tester.openUnscheduledEventsPopup(); - - // Assert that nothing shows up - tester.findUnscheduledPopup(findsNothing, 0); - - // Click on the event in the calendar - await tester.openCalendarEvent(index: 0, date: newDate); - - // Open the date editor of the event - await tester.tapDateCellInRowDetailPage(); - await tester.findDateEditor(findsOneWidget); - - // Clear the date of the event - await tester.clearDate(); - - // Dismiss the event editor - await tester.dismissEventEditor(); - tester.assertNumberOfEventsInCalendar(0); - - // Click on the unscheduled events button - await tester.openUnscheduledEventsPopup(); - - // Assert that a popup appears and 1 unscheduled event - tester.findUnscheduledPopup(findsOneWidget, 1); - - // Click on the unscheduled event - await tester.clickUnscheduledEvent(); - - 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 deleted file mode 100644 index ca565474ec..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart +++ /dev/null @@ -1,548 +0,0 @@ -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'; -import 'package:integration_test/integration_test.dart'; -import 'package:intl/intl.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('edit grid cell:', () { - testWidgets('text', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.RichText, - input: 'hello world', - ); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.RichText, - content: 'hello world', - ); - - await tester.pumpAndSettle(); - }); - - // Make sure the text cells are filled with the right content when there are - // multiple text cell - testWidgets('multiple text cells', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'my grid', - layout: ViewLayoutPB.Grid, - ); - await tester.createField(FieldType.RichText, name: 'description'); - - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.RichText, - input: 'hello', - ); - - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.RichText, - input: 'world', - cellIndex: 1, - ); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.RichText, - content: 'hello', - ); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.RichText, - content: 'world', - cellIndex: 1, - ); - - await tester.pumpAndSettle(); - }); - - testWidgets('number', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - const fieldType = FieldType.Number; - - // Create a number field - await tester.createField(fieldType); - - await tester.editCell( - rowIndex: 0, - fieldType: fieldType, - input: '-1', - ); - // edit the next cell to force the previous cell at row 0 to lose focus - await tester.editCell( - rowIndex: 1, - fieldType: fieldType, - input: '0.2', - ); - // -1 -> -1 - tester.assertCellContent( - rowIndex: 0, - fieldType: fieldType, - content: '-1', - ); - - // edit the next cell to force the previous cell at row 1 to lose focus - await tester.editCell( - rowIndex: 2, - fieldType: fieldType, - input: '.1', - ); - // 0.2 -> 0.2 - tester.assertCellContent( - rowIndex: 1, - fieldType: fieldType, - content: '0.2', - ); - - // edit the next cell to force the previous cell at row 2 to lose focus - await tester.editCell( - rowIndex: 0, - fieldType: fieldType, - input: '', - ); - // .1 -> 0.1 - tester.assertCellContent( - rowIndex: 2, - fieldType: fieldType, - content: '0.1', - ); - - await tester.pumpAndSettle(); - }); - - testWidgets('checkbox', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - await tester.assertCheckboxCell(rowIndex: 0, isSelected: false); - await tester.tapCheckboxCellInGrid(rowIndex: 0); - await tester.assertCheckboxCell(rowIndex: 0, isSelected: true); - - await tester.tapCheckboxCellInGrid(rowIndex: 1); - await tester.tapCheckboxCellInGrid(rowIndex: 2); - await tester.assertCheckboxCell(rowIndex: 1, isSelected: true); - await tester.assertCheckboxCell(rowIndex: 2, isSelected: true); - - await tester.pumpAndSettle(); - }); - - testWidgets('created time', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - const fieldType = FieldType.CreatedTime; - // Create a create time field - // The create time field is not editable - await tester.createField(fieldType); - - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - - await tester.findDateEditor(findsNothing); - - await tester.pumpAndSettle(); - }); - - testWidgets('last modified time', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - const fieldType = FieldType.LastEditedTime; - // Create a last time field - // The last time field is not editable - await tester.createField(fieldType); - - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - - await tester.findDateEditor(findsNothing); - - await tester.pumpAndSettle(); - }); - - testWidgets('date time', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - const fieldType = FieldType.DateTime; - await tester.createField(fieldType); - - // Tap the cell to invoke the field editor - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findDateEditor(findsOneWidget); - - // Toggle include time - await tester.toggleIncludeTime(); - - // Dismiss the cell editor - await tester.dismissCellEditor(); - - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findDateEditor(findsOneWidget); - - // Turn off include time - await tester.toggleIncludeTime(); - - // Select a date - DateTime now = DateTime.now(); - await tester.selectDay(content: now.day); - - await tester.dismissCellEditor(); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y').format(now), - ); - - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - - // Toggle include time - now = DateTime.now(); - await tester.toggleIncludeTime(); - - await tester.dismissCellEditor(); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y HH:mm').format(now), - ); - - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findDateEditor(findsOneWidget); - - // Change date format - await tester.tapChangeDateTimeFormatButton(); - await tester.changeDateFormat(); - - await tester.dismissCellEditor(); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: DateFormat('dd/MM/y HH:mm').format(now), - ); - - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findDateEditor(findsOneWidget); - - // Change time format - await tester.tapChangeDateTimeFormatButton(); - await tester.changeTimeFormat(); - - await tester.dismissCellEditor(); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: DateFormat('dd/MM/y hh:mm a').format(now), - ); - - await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findDateEditor(findsOneWidget); - - // Clear the date and time - await tester.clearDate(); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: '', - ); - - await tester.pumpAndSettle(); - }); - - testWidgets('single select', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const fieldType = FieldType.SingleSelect; - - // When create a grid, it will create a single select field by default - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Tap the cell to invoke the selection option editor - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // Create a new select option - await tester.createOption(name: 'tag 1'); - await tester.dismissCellEditor(); - - // Make sure the option is created and displayed in the cell - tester.findSelectOptionWithNameInGrid( - rowIndex: 0, - name: 'tag 1', - ); - - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // Create another select option - await tester.createOption(name: 'tag 2'); - await tester.dismissCellEditor(); - - tester.findSelectOptionWithNameInGrid( - rowIndex: 0, - name: 'tag 2', - ); - - tester.assertNumberOfSelectedOptionsInGrid( - rowIndex: 0, - matcher: findsOneWidget, - ); - - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // switch to first option - await tester.selectOption(name: 'tag 1'); - await tester.dismissCellEditor(); - - tester.findSelectOptionWithNameInGrid( - rowIndex: 0, - name: 'tag 1', - ); - - tester.assertNumberOfSelectedOptionsInGrid( - rowIndex: 0, - matcher: findsOneWidget, - ); - - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // Deselect the currently-selected option - await tester.selectOption(name: 'tag 1'); - await tester.dismissCellEditor(); - - tester.assertNumberOfSelectedOptionsInGrid( - rowIndex: 0, - matcher: findsNothing, - ); - - await tester.pumpAndSettle(); - }); - - testWidgets('multi select', (tester) async { - final tags = [ - 'tag 1', - 'tag 2', - 'tag 3', - 'tag 4', - ]; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - const fieldType = FieldType.MultiSelect; - await tester.createField(fieldType, name: fieldType.i18n); - - // Tap the cell to invoke the selection option editor - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // Create a new select option - await tester.createOption(name: tags.first); - await tester.dismissCellEditor(); - - // Make sure the option is created and displayed in the cell - tester.findSelectOptionWithNameInGrid( - rowIndex: 0, - name: tags.first, - ); - - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // Create some other select options - await tester.createOption(name: tags[1]); - await tester.createOption(name: tags[2]); - await tester.createOption(name: tags[3]); - await tester.dismissCellEditor(); - - for (final tag in tags) { - tester.findSelectOptionWithNameInGrid( - rowIndex: 0, - name: tag, - ); - } - - tester.assertNumberOfSelectedOptionsInGrid( - rowIndex: 0, - matcher: findsNWidgets(4), - ); - - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // Deselect all options - for (final tag in tags) { - await tester.selectOption(name: tag); - } - await tester.dismissCellEditor(); - - tester.assertNumberOfSelectedOptionsInGrid( - rowIndex: 0, - matcher: findsNothing, - ); - - await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); - await tester.findSelectOptionEditor(findsOneWidget); - - // Select some options - await tester.selectOption(name: tags[1]); - await tester.selectOption(name: tags[3]); - await tester.dismissCellEditor(); - - tester.findSelectOptionWithNameInGrid( - rowIndex: 0, - name: tags[1], - ); - tester.findSelectOptionWithNameInGrid( - rowIndex: 0, - name: tags[3], - ); - - tester.assertNumberOfSelectedOptionsInGrid( - rowIndex: 0, - matcher: findsNWidgets(2), - ); - - await tester.pumpAndSettle(); - }); - - testWidgets('checklist', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - const fieldType = FieldType.Checklist; - await tester.createField(fieldType); - - // assert that there is no progress bar in the grid - tester.assertChecklistCellInGrid(rowIndex: 0, percent: null); - - // tap on the first checklist cell - await tester.tapChecklistCellInGrid(rowIndex: 0); - - // assert that the checklist editor is shown - tester.assertChecklistEditorVisible(visible: true); - - // create a new task with enter - await tester.createNewChecklistTask(name: "task 1", enter: true); - - // assert that the task is displayed - tester.assertChecklistTaskInEditor( - index: 0, - name: "task 1", - isChecked: false, - ); - - // update the task's name - await tester.renameChecklistTask(index: 0, name: "task 11"); - - // assert that the task's name is updated - tester.assertChecklistTaskInEditor( - index: 0, - name: "task 11", - isChecked: false, - ); - - // dismiss new task editor - await tester.dismissCellEditor(); - - // dismiss checklist cell editor - await tester.dismissCellEditor(); - - // assert that progress bar is shown in grid at 0% - tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0); - - // start editing the first checklist cell again - await tester.tapChecklistCellInGrid(rowIndex: 0); - - // create another task with the create button - await tester.createNewChecklistTask(name: "task 2", button: true); - - // assert that the task was inserted - tester.assertChecklistTaskInEditor( - index: 1, - name: "task 2", - isChecked: false, - ); - - // mark it as complete - await tester.checkChecklistTask(index: 1); - - // assert that the task was checked in the editor - tester.assertChecklistTaskInEditor( - index: 1, - name: "task 2", - isChecked: true, - ); - - // dismiss checklist editor - await tester.dismissCellEditor(); - await tester.dismissCellEditor(); - - // assert that progressbar is shown in grid at 50% - tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5); - - // re-open the cell editor - await tester.tapChecklistCellInGrid(rowIndex: 0); - - // hover over first task and delete it - await tester.deleteChecklistTask(index: 0); - - // dismiss cell editor - await tester.dismissCellEditor(); - - // assert that progressbar is shown in grid at 100% - tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1); - - // re-open the cell edior - await tester.tapChecklistCellInGrid(rowIndex: 0); - - // delete the remaining task - await tester.deleteChecklistTask(index: 0); - - // dismiss the cell editor - await tester.dismissCellEditor(); - - // check that the progress bar is not viisble - tester.assertChecklistCellInGrid(rowIndex: 0, percent: null); - }); - }); -} 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 deleted file mode 100644 index a71110f1e0..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart +++ /dev/null @@ -1,105 +0,0 @@ -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-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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid field settings test:', () { - 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); - - // create a field - await tester.scrollToRight(find.byType(GridPage)); - await tester.tapNewPropertyButton(); - await tester.renameField('New field 1'); - await tester.dismissFieldEditor(); - - // hide the field - await tester.tapGridFieldWithName('New field 1'); - 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'); - - // go back to linked database view, expect field to be hidden - await tester.tapTabBarLinkedViewByViewName('Grid'); - tester.noFieldWithName('New field 1'); - - // use the settings button to show the field - await tester.tapDatabaseSettingButton(); - await tester.tapViewPropertiesButton(); - await tester.tapViewTogglePropertyVisibilityButtonByName('New field 1'); - await tester.dismissFieldEditor(); - tester.findFieldWithName('New field 1'); - - // open first row in popup then hide the field - await tester.openFirstRowDetailPage(); - await tester.tapGridFieldWithNameInRowDetailPage('New field 1'); - 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 deleted file mode 100644 index 6ce248a8a1..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart +++ /dev/null @@ -1,600 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('grid edit field test:', () { - testWidgets('rename existing field', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Invoke the field editor - await tester.tapGridFieldWithName('Name'); - - await tester.renameField('hello world'); - await tester.dismissFieldEditor(); - - await tester.tapGridFieldWithName('hello world'); - 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(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Invoke the field editor - await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Checkbox); - - await tester.assertFieldTypeWithFieldName( - 'Type', - FieldType.Checkbox, - ); - await tester.pumpAndSettle(); - }); - - testWidgets('create a field and rename it', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new grid - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // create a field - await tester.createField(FieldType.Checklist); - tester.findFieldWithName(FieldType.Checklist.i18n); - - // editing field type during field creation should change title - await tester.createField(FieldType.MultiSelect); - tester.findFieldWithName(FieldType.MultiSelect.i18n); - - // not if the user changes the title manually though - const name = "New field"; - await tester.createField(FieldType.DateTime); - await tester.tapGridFieldWithName(FieldType.DateTime.i18n); - await tester.renameField(name); - await tester.tapEditFieldButton(); - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.URL); - tester.findFieldWithName(name); - }); - - testWidgets('delete field', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // create a field - await tester.createField(FieldType.Checkbox, name: 'New field 1'); - - // Delete the field - await tester.tapGridFieldWithName('New field 1'); - await tester.tapDeletePropertyButton(); - - // confirm delete - await tester.tapButtonWithName(LocaleKeys.space_delete.tr()); - - tester.noFieldWithName('New field 1'); - await tester.pumpAndSettle(); - }); - - testWidgets('duplicate field', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // create a field - await tester.createField(FieldType.RichText, name: 'New field 1'); - - // duplicate the field - await tester.tapGridFieldWithName('New field 1'); - await tester.tapDuplicatePropertyButton(); - - tester.findFieldWithName('New field 1 (copy)'); - await tester.pumpAndSettle(); - }); - - testWidgets('insert field on either side of a field', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - await tester.scrollToRight(find.byType(GridPage)); - - // insert new field to the right - await tester.tapGridFieldWithName('Type'); - await tester.tapInsertFieldButton(left: false, name: 'Right'); - await tester.dismissFieldEditor(); - tester.findFieldWithName('Right'); - - // insert new field to the left - await tester.tapGridFieldWithName('Type'); - await tester.tapInsertFieldButton(left: true, name: "Left"); - await tester.dismissFieldEditor(); - tester.findFieldWithName('Left'); - - await tester.pumpAndSettle(); - }); - - testWidgets('create list of fields', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - for (final fieldType in [ - FieldType.Checklist, - FieldType.DateTime, - FieldType.Number, - FieldType.URL, - FieldType.MultiSelect, - FieldType.LastEditedTime, - FieldType.CreatedTime, - FieldType.Checkbox, - ]) { - await tester.createField(fieldType); - - // After update the field type, the cells should be updated - tester.findCellByFieldType(fieldType); - await tester.pumpAndSettle(); - } - }); - - testWidgets('field types with empty type option editor', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - for (final fieldType in [ - FieldType.RichText, - FieldType.Checkbox, - FieldType.Checklist, - FieldType.URL, - ]) { - await tester.createField(fieldType); - - // open the field editor - await tester.tapGridFieldWithName(fieldType.i18n); - await tester.tapEditFieldButton(); - - // check type option editor is empty - tester.expectEmptyTypeOptionEditor(); - await tester.dismissFieldEditor(); - } - }); - - testWidgets('number field type option', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - await tester.scrollToRight(find.byType(GridPage)); - - // create a number field - await tester.createField(FieldType.Number); - - // enter some data into the first number cell - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.Number, - input: '123', - ); - // edit the next cell to force the previous cell at row 0 to lose focus - await tester.editCell( - rowIndex: 1, - fieldType: FieldType.Number, - input: '0.2', - ); - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.Number, - content: '123', - ); - - // open editor and change number format - await tester.tapGridFieldWithName(FieldType.Number.i18n); - await tester.tapEditFieldButton(); - await tester.changeNumberFieldFormat(); - await tester.dismissFieldEditor(); - - // assert number format has been changed - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.Number, - content: '\$123', - ); - }); - - testWidgets('add option', (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(); - - // tap 'add option' button - await tester.tapAddSelectOptionButton(); - const text = 'Hello AppFlowy'; - final inputField = find.descendant( - of: find.byType(CreateOptionTextField), - matching: find.byType(TextField), - ); - await tester.enterText(inputField, text); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - - // check the result - tester.expectToSeeText(text); - }); - - testWidgets('date time field type options', (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); - await tester.toggleIncludeTime(); - final now = DateTime.now(); - 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), - ); - - // open editor and change date & time format - await tester.tapGridFieldWithName(FieldType.DateTime.i18n); - await tester.tapEditFieldButton(); - await tester.changeDateFormat(); - await tester.changeTimeFormat(); - await tester.dismissFieldEditor(); - - // assert date format has been changed - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: DateFormat('dd/MM/y hh:mm a').format(now), - ); - }); - - 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 { - // await tester.initializeAppFlowy(); - // await tester.tapGoButton(); - - // await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - // final created = DateTime.now(); - - // // create a created at field - // await tester.tapNewPropertyButton(); - // await tester.renameField(FieldType.CreatedTime.i18n); - // await tester.tapSwitchFieldTypeButton(); - // await tester.selectFieldType(FieldType.CreatedTime); - // await tester.dismissFieldEditor(); - - // // create a last modified field - // await tester.tapNewPropertyButton(); - // await tester.renameField(FieldType.LastEditedTime.i18n); - // await tester.tapSwitchFieldTypeButton(); - - // // get time just before modifying - // final modified = DateTime.now(); - - // // create a last modified field (cont'd) - // await tester.selectFieldType(FieldType.LastEditedTime); - // await tester.dismissFieldEditor(); - - // tester.assertCellContent( - // rowIndex: 0, - // fieldType: FieldType.CreatedTime, - // content: DateFormat('MMM dd, y HH:mm').format(created), - // ); - // tester.assertCellContent( - // rowIndex: 0, - // fieldType: FieldType.LastEditedTime, - // content: DateFormat('MMM dd, y HH:mm').format(modified), - // ); - - // // open field editor and change date & time format - // await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n); - // await tester.tapEditFieldButton(); - // await tester.changeDateFormat(); - // await tester.changeTimeFormat(); - // await tester.dismissFieldEditor(); - - // // open field editor and change date & time format - // await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n); - // await tester.tapEditFieldButton(); - // await tester.changeDateFormat(); - // await tester.changeTimeFormat(); - // await tester.dismissFieldEditor(); - - // // assert format has been changed - // tester.assertCellContent( - // rowIndex: 0, - // fieldType: FieldType.CreatedTime, - // content: DateFormat('dd/MM/y hh:mm a').format(created), - // ); - // tester.assertCellContent( - // rowIndex: 0, - // fieldType: FieldType.LastEditedTime, - // 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 deleted file mode 100644 index 8e79445503..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart +++ /dev/null @@ -1,224 +0,0 @@ -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: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); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name'); - await tester.tapFilterButtonInGrid('Name'); - - // enter 'A' in the filter text field - tester.assertNumberOfRowsInGridPage(10); - await tester.enterTextInTextFilter('A'); - tester.assertNumberOfRowsInGridPage(1); - - // after remove the filter, the grid should show all rows - await tester.enterTextInTextFilter(''); - tester.assertNumberOfRowsInGridPage(10); - - await tester.enterTextInTextFilter('B'); - tester.assertNumberOfRowsInGridPage(1); - - // open the menu to delete the filter - await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor)); - await tester.tapDeleteFilterButtonInGrid(); - tester.assertNumberOfRowsInGridPage(10); - - await tester.pumpAndSettle(); - }); - - testWidgets('add checkbox filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done'); - tester.assertNumberOfRowsInGridPage(5); - - await tester.tapFilterButtonInGrid('Done'); - await tester.tapCheckboxFilterButtonInGrid(); - - await tester.tapUnCheckedButtonOnCheckboxFilter(); - tester.assertNumberOfRowsInGridPage(5); - - await tester - .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); - await tester.tapDeleteFilterButtonInGrid(); - tester.assertNumberOfRowsInGridPage(10); - - await tester.pumpAndSettle(); - }); - - testWidgets('add checklist filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); - - // 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.tapFilterButtonInGrid('checklist'); - await tester.tapChecklistFilterButtonInGrid(); - - await tester.tapCompletedButtonOnChecklistFilter(); - tester.assertNumberOfRowsInGridPage(1); - - await tester.pumpAndSettle(); - }); - - testWidgets('add single select filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type'); - - await tester.tapFilterButtonInGrid('Type'); - - // select the option 's6' - await tester.tapOptionFilterWithName('s6'); - tester.assertNumberOfRowsInGridPage(0); - - // unselect the option 's6' - await tester.tapOptionFilterWithName('s6'); - tester.assertNumberOfRowsInGridPage(10); - - // select the option 's5' - await tester.tapOptionFilterWithName('s5'); - tester.assertNumberOfRowsInGridPage(1); - - // select the option 's4' - await tester.tapOptionFilterWithName('s4'); - - // The row with 's4' should be shown. - tester.assertNumberOfRowsInGridPage(2); - - await tester.pumpAndSettle(); - }); - - testWidgets('add multi select filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.MultiSelect, - 'multi-select', - ); - - await tester.tapFilterButtonInGrid('multi-select'); - await tester.scrollOptionFilterListByOffset(const Offset(0, -200)); - - // select the option 'm1'. Any option with 'm1' should be shown. - await tester.tapOptionFilterWithName('m1'); - 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.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.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_reminder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_reminder_test.dart deleted file mode 100644 index efd365c043..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_reminder_test.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.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'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('reminder in database', () { - testWidgets('add date field and add reminder', (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 date type - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.DateTime); - await tester.dismissFieldEditor(); - - // Open date picker - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - await tester.findDateEditor(findsOneWidget); - - // Select date - final isToday = await tester.selectLastDateInPicker(); - - // Select "On day of event" reminder - await tester.selectReminderOption(ReminderOption.onDayOfEvent); - - // Expect "On day of event" to be displayed - tester.expectSelectedReminder(ReminderOption.onDayOfEvent); - - // Dismiss the cell/date editor - await tester.dismissCellEditor(); - - // Open date picker again - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - await tester.findDateEditor(findsOneWidget); - - // Expect "On day of event" to be displayed - tester.expectSelectedReminder(ReminderOption.onDayOfEvent); - - // Dismiss the cell/date editor - await tester.dismissCellEditor(); - - int tabIndex = 1; - final now = DateTime.now(); - if (isToday && now.hour >= 9) { - tabIndex = 0; - } - - // Open "Upcoming" in Notification hub - await tester.openNotificationHub(tabIndex: tabIndex); - - // Expect 1 notification - tester.expectNotificationItems(1); - }); - - testWidgets('navigate from reminder to open row', (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 date type - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.DateTime); - await tester.dismissFieldEditor(); - - // Open date picker - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - await tester.findDateEditor(findsOneWidget); - - // Select date - final isToday = await tester.selectLastDateInPicker(); - - // Select "On day of event"-reminder - await tester.selectReminderOption(ReminderOption.onDayOfEvent); - - // Expect "On day of event" to be displayed - tester.expectSelectedReminder(ReminderOption.onDayOfEvent); - - // Dismiss the cell/date editor - await tester.dismissCellEditor(); - - // Open date picker again - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - await tester.findDateEditor(findsOneWidget); - - // Expect "On day of event" to be displayed - tester.expectSelectedReminder(ReminderOption.onDayOfEvent); - - // Dismiss the cell/date editor - await tester.dismissCellEditor(); - - // Create and Navigate to a new document - await tester.createNewPageWithNameUnderParent(); - await tester.pumpAndSettle(); - - int tabIndex = 1; - final now = DateTime.now(); - if (isToday && now.hour >= 9) { - tabIndex = 0; - } - - // Open correct tab in Notification hub - await tester.openNotificationHub(tabIndex: tabIndex); - - // Expect 1 notification - tester.expectNotificationItems(1); - - // Tap on the notification - await tester.tap(find.byType(NotificationItem)); - await tester.pumpAndSettle(); - - // Expect to see Row Editor Dialog - tester.expectToSeeRowDetailsPageDialog(); - }); - - testWidgets( - 'toggle include time sets reminder option correctly', - (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 date type - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.DateTime); - await tester.dismissFieldEditor(); - - // Open date picker - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - await tester.findDateEditor(findsOneWidget); - - // Select date - await tester.selectLastDateInPicker(); - - // Select "On day of event"-reminder - await tester.selectReminderOption(ReminderOption.onDayOfEvent); - - // Expect "On day of event" to be displayed - tester.expectSelectedReminder(ReminderOption.onDayOfEvent); - - // Dismiss the cell/date editor - await tester.dismissCellEditor(); - - // Open date picker again - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - await tester.findDateEditor(findsOneWidget); - - // Expect "On day of event" to be displayed - tester.expectSelectedReminder(ReminderOption.onDayOfEvent); - - // Toggle include time on - await tester.toggleIncludeTime(); - - // Expect "At time of event" to be displayed - tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); - - // Dismiss the cell/date editor - await tester.dismissCellEditor(); - - // Open date picker again - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - await tester.findDateEditor(findsOneWidget); - - // Expect "At time of event" to be displayed - tester.expectSelectedReminder(ReminderOption.atTimeOfEvent); - - // Select "One hour before"-reminder - await tester.selectReminderOption(ReminderOption.oneHourBefore); - - // Expect "One hour before" to be displayed - tester.expectSelectedReminder(ReminderOption.oneHourBefore); - - // Toggle include time off - await tester.toggleIncludeTime(); - - // Expect "On day of event" to be displayed - tester.expectSelectedReminder(ReminderOption.onDayOfEvent); - }, - ); - }); -} 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 deleted file mode 100644 index 22f059d199..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart +++ /dev/null @@ -1,508 +0,0 @@ -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: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:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/emoji.dart'; -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('grid row detail page:', () { - testWidgets('opens', (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(); - - // Make sure that the row page is opened - tester.assertRowDetailPageOpened(); - - // Each row detail page should have a document - await tester.assertDocumentExistInRowDetailPage(); - }); - - testWidgets('add and update 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('😀'); - - // 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(); - - await tester.tapEmoji('😅'); - - // The emoji already displayed in the row banner - final emojiText = find.byWidgetPredicate( - (widget) => widget is FlowyText && widget.text == '😅', - ); - - // 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 { - 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('😀'); - - // Remove the emoji - await tester.tapButton(find.byType(EmojiIconWidget)); - await tester.tapButton(find.text(LocaleKeys.button_remove.tr())); - - final emojiText = find.byWidgetPredicate( - (widget) => widget is FlowyText && widget.text == '😀', - ); - expect(emojiText, findsNothing); - }); - - testWidgets('create list of fields', (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(); - - for (final fieldType in [ - FieldType.Checklist, - FieldType.DateTime, - FieldType.Number, - FieldType.URL, - FieldType.MultiSelect, - FieldType.LastEditedTime, - FieldType.CreatedTime, - FieldType.Checkbox, - ]) { - await tester.tapRowDetailPageCreatePropertyButton(); - - // 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.scrollRowDetailByOffset(const Offset(0, -50)); - } - }); - - testWidgets('change order of fields and cells', (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(); - - // Assert that the first field in the row details page is the select - // option type - tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect); - - // Reorder first field in list - final gesture = await tester.hoverOnFieldInRowDetail(index: 0); - await tester.pumpAndSettle(); - await tester.reorderFieldInRowDetail(offset: 30); - - // Orders changed, now the checkbox is first - tester.assertFirstFieldInRowDetailByType(FieldType.Checkbox); - await gesture.removePointer(); - await tester.pumpAndSettle(); - - // Reorder second field in list - await tester.hoverOnFieldInRowDetail(index: 1); - await tester.pumpAndSettle(); - await tester.reorderFieldInRowDetail(offset: -30); - - // First field is now back to select option - tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect); - }); - - testWidgets('hide and show hidden fields', (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(); - - // Assert that the show hidden fields button isn't visible - tester.assertToggleShowHiddenFieldsVisibility(false); - - // Hide the first field in the field list - await tester.tapGridFieldWithNameInRowDetailPage("Type"); - await tester.tapHidePropertyButtonInFieldEditor(); - - // Assert that the field is now hidden - tester.noFieldWithName("Type"); - - // Assert that the show hidden fields button appears - tester.assertToggleShowHiddenFieldsVisibility(true); - - // Click on the show hidden fields button - await tester.toggleShowHiddenFields(); - - // Assert that the hidden field is shown again and that the show - // hidden fields button is still present - tester.findFieldWithName("Type"); - tester.assertToggleShowHiddenFieldsVisibility(true); - - // Click hide hidden fields - await tester.toggleShowHiddenFields(); - - // Assert that the hidden field has vanished - tester.noFieldWithName("Type"); - - // Click show hidden fields - await tester.toggleShowHiddenFields(); - - // delete the hidden field - await tester.tapGridFieldWithNameInRowDetailPage("Type"); - await tester.tapDeletePropertyInFieldEditor(); - - // Assert that the that the show hidden fields button is gone - tester.assertToggleShowHiddenFieldsVisibility(false); - }); - - testWidgets('update the contents of the document and re-open it', - (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(); - - // Wait for the document to be loaded - await tester.wait(500); - - // Focus on the editor - final textBlock = find.byType(ParagraphBlockComponentWidget); - await tester.tapAt(tester.getCenter(textBlock)); - await tester.pumpAndSettle(); - - // Input some text - const inputText = 'Hello World'; - await tester.ime.insertText(inputText); - expect( - find.textContaining(inputText, findRichText: true), - findsOneWidget, - ); - - // Tap outside to dismiss the field - await tester.tapAt(Offset.zero); - await tester.pumpAndSettle(); - - // Re-open the document - await tester.openFirstRowDetailPage(); - expect( - find.textContaining(inputText, findRichText: true), - findsOneWidget, - ); - }); - - testWidgets( - 'check if the title wraps properly when a long text is inserted', - (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(); - - // Wait for the document to be loaded - await tester.wait(500); - - // Focus on the editor - final textField = find - .descendant( - of: find.byType(SimpleDialog), - matching: find.byType(TextField), - ) - .first; - - // Input a long text - await tester.enterText(textField, 'Long text' * 25); - await tester.pumpAndSettle(); - - // Tap outside to dismiss the field - await tester.tapAt(Offset.zero); - await tester.pumpAndSettle(); - - // Check if there is any overflow in the widget tree - expect(tester.takeException(), isNull); - - // Re-open the document - await tester.openFirstRowDetailPage(); - - // Check again if there is any overflow in the widget tree - expect(tester.takeException(), isNull); - }); - - testWidgets('delete row', (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.tapRowDetailPageRowActionButton(); - await tester.tapRowDetailPageDeleteRowButton(); - await tester.tapEscButton(); - - tester.assertNumberOfRowsInGridPage(2); - }); - - testWidgets('duplicate row', (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.tapRowDetailPageRowActionButton(); - 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)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_setting_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_setting_test.dart deleted file mode 100644 index 7c8058425e..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_setting_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.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/database_test_op.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid', () { - testWidgets('update layout', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // open setting - await tester.tapDatabaseSettingButton(); - // select the layout - await tester.tapDatabaseLayoutButton(); - // select layout by board - await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); - await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); - - await tester.pumpAndSettle(); - }); - - testWidgets('update layout multiple times', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // open setting - await tester.tapDatabaseSettingButton(); - await tester.tapDatabaseLayoutButton(); - await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); - await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); - - await tester.tapDatabaseSettingButton(); - await tester.tapDatabaseLayoutButton(); - await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Calendar); - await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar); - - 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 deleted file mode 100644 index 2beb74a5f2..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart +++ /dev/null @@ -1,166 +0,0 @@ -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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('database', () { - testWidgets('import v0.2.0 database data', (tester) async { - await tester.openTestDatabase(v020GridFileName); - // wait the database data is loaded - await tester.pumpAndSettle(const Duration(microseconds: 500)); - - // check the text cell - final textCells = ['A', 'B', 'C', 'D', 'E', '', '', '', '', '']; - for (final (index, content) in textCells.indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.RichText, - content: content, - ); - } - - // check the checkbox cell - final checkboxCells = [ - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - ]; - for (final (index, content) in checkboxCells.indexed) { - await tester.assertCheckboxCell( - rowIndex: index, - isSelected: content, - ); - } - - // check the number cell - final numberCells = [ - '-1', - '-2', - '0.1', - '0.2', - '1', - '2', - '10', - '11', - '12', - '', - ]; - for (final (index, content) in numberCells.indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.Number, - content: content, - ); - } - - // check the url cell - final urlCells = [ - 'appflowy.io', - 'no url', - 'appflowy.io', - 'https://github.com/AppFlowy-IO/', - '', - '', - ]; - for (final (index, content) in urlCells.indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.URL, - content: content, - ); - } - - // check the single select cell - final singleSelectCells = [ - 's1', - 's2', - 's3', - 's4', - 's5', - '', - '', - '', - '', - '', - ]; - for (final (index, content) in singleSelectCells.indexed) { - await tester.assertSingleSelectOption( - rowIndex: index, - content: content, - ); - } - - // check the multi select cell - final List> multiSelectCells = [ - ['m1'], - ['m1', 'm2'], - ['m1', 'm2', 'm3'], - ['m1', 'm2', 'm3'], - ['m1', 'm2', 'm3', 'm4', 'm5'], - [], - [], - [], - [], - [], - ]; - for (final (index, contents) in multiSelectCells.indexed) { - tester.assertMultiSelectOption( - rowIndex: index, - contents: contents, - ); - } - - // check the checklist cell - final List checklistCells = [ - 0.67, - 0.33, - 1.0, - null, - null, - null, - null, - null, - null, - null, - ]; - for (final (index, percent) in checklistCells.indexed) { - tester.assertChecklistCellInGrid( - rowIndex: index, - percent: percent, - ); - } - - // check the date cell - final List dateCells = [ - 'Jun 01, 2023', - 'Jun 02, 2023', - 'Jun 03, 2023', - 'Jun 04, 2023', - 'Jun 05, 2023', - 'Jun 05, 2023', - 'Jun 16, 2023', - '', - '', - '', - ]; - for (final (index, content) in dateCells.indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.DateTime, - content: content, - ); - } - }); - }); -} 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 deleted file mode 100644 index e09d8718be..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart +++ /dev/null @@ -1,471 +0,0 @@ -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'; - -import '../../shared/database_test_op.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid sort:', () { - testWidgets('text sort', (tester) async { - await tester.openTestDatabase(v020GridFileName); - // create a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - // check the text cell order - final textCells = [ - 'A', - 'B', - 'C', - 'D', - 'E', - '', - '', - '', - '', - '', - ]; - for (final (index, content) in textCells.indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.RichText, - content: content, - ); - } - - // open the sort menu and select order by descending - await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('Name'); - await tester.tapSortByDescending(); - for (final (index, content) in [ - 'E', - 'D', - 'C', - 'B', - 'A', - '', - '', - '', - '', - '', - ].indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.RichText, - content: content, - ); - } - - // delete all sorts - await tester.tapSortMenuInSettingBar(); - await tester.tapDeleteAllSortsButton(); - - // check the text cell order - for (final (index, content) in [ - 'A', - 'B', - 'C', - 'D', - 'E', - '', - '', - '', - '', - '', - ].indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.RichText, - content: content, - ); - } - await tester.pumpAndSettle(); - }); - - testWidgets('checkbox', (tester) async { - await tester.openTestDatabase(v020GridFileName); - // create a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); - - // check the checkbox cell order - for (final (index, content) in [ - false, - false, - false, - false, - false, - true, - true, - true, - true, - true, - ].indexed) { - await tester.assertCheckboxCell( - rowIndex: index, - isSelected: content, - ); - } - - // open the sort menu and select order by descending - await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('Done'); - await tester.tapSortByDescending(); - for (final (index, content) in [ - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - ].indexed) { - await tester.assertCheckboxCell( - rowIndex: index, - isSelected: content, - ); - } - - await tester.pumpAndSettle(); - }); - - testWidgets('number', (tester) async { - await tester.openTestDatabase(v020GridFileName); - // create a 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, - ); - } - - // open the sort menu and select order by descending - await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('number'); - await tester.tapSortByDescending(); - for (final (index, content) in [ - '12', - '11', - '10', - '2', - '1', - '0.2', - '0.1', - '-1', - '-2', - '', - ].indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.Number, - content: content, - ); - } - - await tester.pumpAndSettle(); - }); - - testWidgets('checkbox and number', (tester) async { - await tester.openTestDatabase(v020GridFileName); - // 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.tapSortByDescending(); - for (final (index, content) in [ - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - ].indexed) { - await tester.assertCheckboxCell( - rowIndex: index, - isSelected: content, - ); - } - - // add another sort, this time by number descending - await tester.tapSortMenuInSettingBar(); - await tester.tapCreateSortByFieldTypeInSortMenu( - FieldType.Number, - 'number', - ); - await tester.tapEditSortConditionButtonByFieldName('number'); - await tester.tapSortByDescending(); - - // check checkbox cell order - for (final (index, content) in [ - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - ].indexed) { - await tester.assertCheckboxCell( - rowIndex: index, - isSelected: content, - ); - } - - // check number cell order - for (final (index, content) in [ - '1', - '0.2', - '0.1', - '-1', - '-2', - '12', - '11', - '10', - '2', - '', - ].indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.Number, - content: content, - ); - } - - await tester.pumpAndSettle(); - }); - - testWidgets('reorder sort', (tester) async { - await tester.openTestDatabase(v020GridFileName); - // 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.tapSortByDescending(); - - // add another sort, this time by number descending - await tester.tapSortMenuInSettingBar(); - await tester.tapCreateSortByFieldTypeInSortMenu( - FieldType.Number, - 'number', - ); - await tester.tapEditSortConditionButtonByFieldName('number'); - await tester.tapSortByDescending(); - - // check checkbox cell order - for (final (index, content) in [ - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - ].indexed) { - await tester.assertCheckboxCell( - rowIndex: index, - isSelected: content, - ); - } - - // check number cell order - for (final (index, content) in [ - '1', - '0.2', - '0.1', - '-1', - '-2', - '12', - '11', - '10', - '2', - '', - ].indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.Number, - content: content, - ); - } - - // reorder sort - await tester.tapSortMenuInSettingBar(); - await tester.reorderSort( - (FieldType.Number, 'number'), - (FieldType.Checkbox, 'Done'), - ); - - // check checkbox cell order - for (final (index, content) in [ - false, - false, - false, - false, - true, - true, - true, - true, - true, - false, - ].indexed) { - await tester.assertCheckboxCell( - rowIndex: index, - isSelected: content, - ); - } - - // check the number cell order - for (final (index, content) in [ - '12', - '11', - '10', - '2', - '1', - '0.2', - '0.1', - '-1', - '-2', - '', - ].indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.Number, - content: content, - ); - } - }); - - 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 deleted file mode 100644 index 71656c1ea6..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -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'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('database', () { - testWidgets('create linked view', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Create board view - await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board); - tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); - - // Create grid view - await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid); - tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid); - - // Create calendar view - await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Calendar); - tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar); - - await tester.pumpAndSettle(); - }); - - testWidgets('rename and delete linked view', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Create board view - await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board); - tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); - - // rename board view - await tester.renameLinkedView( - tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board), - 'new board', - ); - final findBoard = tester.findTabBarLinkViewByViewName('new board'); - expect(findBoard, findsOneWidget); - - // delete the board - await tester.deleteDatebaseView(findBoard); - expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing); - - await tester.pumpAndSettle(); - }); - - testWidgets('delete the last database view', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Create board view - await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board); - tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); - - // delete the board - await tester.deleteDatebaseView( - tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board), - ); - - 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 deleted file mode 100644 index 1a8a3fcda8..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -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'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/keyboard.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document alignment', () { - testWidgets('edit alignment in toolbar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final selection = Selection.single( - path: [0], - startOffset: 0, - endOffset: 1, - ); - // click the first line of the readme - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.updateSelection(selection); - await tester.pumpAndSettle(); - - // click the align center - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); - await tester - .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m); - - // expect to see the align center - final editorState = tester.editor.getCurrentEditorState(); - final first = editorState.getNodeAtPath([0])!; - 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); - 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); - expect(first.attributes[blockComponentAlign], 'left'); - }); - - testWidgets('edit alignment using shortcut', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // click the first line of the readme - await tester.editor.tapLineOfEditorAt(0); - - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - final first = editorState.getNodeAtPath([0])!; - - // expect to see text aligned to the right - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - LogicalKeyboardKey.control, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyR, - ], - tester: tester, - withKeyUp: true, - ); - expect(first.attributes[blockComponentAlign], rightAlignmentKey); - - // expect to see text aligned to the center - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - LogicalKeyboardKey.control, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyC, - ], - tester: tester, - withKeyUp: true, - ); - expect(first.attributes[blockComponentAlign], centerAlignmentKey); - - // expect to see text aligned to the left - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - LogicalKeyboardKey.control, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyL, - ], - tester: tester, - withKeyUp: true, - ); - expect(first.attributes[blockComponentAlign], leftAlignmentKey); - }); - }); -} 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 deleted file mode 100644 index a498086952..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -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_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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - 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)); - - // 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); - - // paste the text - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - expect(editorState.document.root.children.length, 1); - expect( - editorState.getNodeAtPath([0])!.delta!.toPlainText(), - text, - ); - }); - }); -} - -/// Inserts an codeBlock in the document -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, - ); - // 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 deleted file mode 100644 index d1e34edcb5..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ /dev/null @@ -1,567 +0,0 @@ -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:', () { - 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'); - for (var i = 0; i < lines; i++) { - expect( - textLines[i], - 'line $i', - ); - } - }, - ); - }); - - // ## **User Installation** - // - [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages) - // - [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker) - // - [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source) - testWidgets('paste content from html, sample 1', (tester) async { - await tester.pasteContent( - html: - '''

User Installation

-''', - (editorState) { - expect(editorState.document.root.children.length, 4); - final node1 = editorState.getNodeAtPath([0])!; - final node2 = editorState.getNodeAtPath([1])!; - final node3 = editorState.getNodeAtPath([2])!; - final node4 = editorState.getNodeAtPath([3])!; - expect(node1.delta!.toJson(), [ - { - "insert": "User Installation", - "attributes": {"bold": true}, - } - ]); - expect(node2.delta!.toJson(), [ - { - "insert": "Windows/Mac/Linux", - "attributes": { - "href": - "https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages", - }, - } - ]); - expect( - node3.delta!.toJson(), - [ - { - "insert": "Docker", - "attributes": { - "href": - "https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker", - }, - } - ], - ); - expect( - node4.delta!.toJson(), - [ - { - "insert": "Source", - "attributes": { - "href": - "https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source", - }, - } - ], - ); - }, - ); - }); - - testWidgets('paste code from VSCode', (tester) async { - await tester.pasteContent( - html: - '''
void main() {
runApp(const MyApp());
}
''', - (editorState) { - expect(editorState.document.root.children.length, 3); - final node1 = editorState.getNodeAtPath([0])!; - final node2 = editorState.getNodeAtPath([1])!; - final node3 = editorState.getNodeAtPath([2])!; - expect(node1.type, ParagraphBlockKeys.type); - expect(node2.type, ParagraphBlockKeys.type); - expect(node3.type, ParagraphBlockKeys.type); - expect(node1.delta!.toJson(), [ - {'insert': 'void main() {'}, - ]); - expect(node2.delta!.toJson(), [ - {'insert': " runApp(const MyApp());"}, - ]); - expect(node3.delta!.toJson(), [ - {"insert": "}"}, - ]); - }); - }); - - testWidgets('paste bulleted list in numbered list', (tester) async { - const inAppJson = - '{"document":{"type":"page","children":[{"type":"bulleted_list","children":[{"type":"bulleted_list","data":{"delta":[{"insert":"World"}]}}],"data":{"delta":[{"insert":"Hello"}]}}]}}'; - - await tester.pasteContent( - inAppJson: inAppJson, - beforeTest: (editorState) async { - final transaction = editorState.transaction; - // Insert two numbered list nodes - // 1. Parent One - // 2. - transaction.insertNodes( - [0], - [ - Node( - type: NumberedListBlockKeys.type, - attributes: { - 'delta': [ - {"insert": "One"}, - ], - }, - ), - Node( - type: NumberedListBlockKeys.type, - attributes: {'delta': []}, - ), - ], - ); - - // Set the selection to the second numbered list node (which has empty delta) - transaction.afterSelection = Selection.collapsed(Position(path: [1])); - - await editorState.apply(transaction); - await tester.pumpAndSettle(); - }, - (editorState) { - final secondNode = editorState.getNodeAtPath([1]); - expect(secondNode?.delta?.toPlainText(), 'Hello'); - expect(secondNode?.children.length, 1); - - final childNode = secondNode?.children.first; - expect(childNode?.delta?.toPlainText(), 'World'); - expect(childNode?.type, BulletedListBlockKeys.type); - }, - ); - }); - - testWidgets('paste text on part of bullet list', (tester) async { - const plainText = 'test'; - - await tester.pasteContent( - plainText: plainText, - beforeTest: (editorState) async { - final transaction = editorState.transaction; - transaction.insertNodes( - [0], - [ - Node( - type: BulletedListBlockKeys.type, - attributes: { - 'delta': [ - {"insert": "bullet list"}, - ], - }, - ), - ], - ); - - // Set the selection to the second numbered list node (which has empty delta) - transaction.afterSelection = Selection( - start: Position(path: [0], offset: 7), - end: Position(path: [0], offset: 11), - ); - - await editorState.apply(transaction); - await tester.pumpAndSettle(); - }, - (editorState) { - final node = editorState.getNodeAtPath([0]); - expect(node?.delta?.toPlainText(), 'bullet test'); - expect(node?.type, BulletedListBlockKeys.type); - }, - ); - }); - - testWidgets('paste image(png) from memory', (tester) async { - final image = await rootBundle.load('assets/test/images/sample.png'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent(image: ('png', bytes), (editorState) { - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotNull); - }); - }); - - testWidgets('paste image(jpeg) from memory', (tester) async { - final image = await rootBundle.load('assets/test/images/sample.jpeg'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent(image: ('jpeg', bytes), (editorState) { - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotNull); - }); - }); - - testWidgets('paste image(gif) from memory', (tester) async { - final image = await rootBundle.load('assets/test/images/sample.gif'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent(image: ('gif', bytes), (editorState) { - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotNull); - }); - }); - - testWidgets( - 'format the selected text to href when pasting url if available', - (tester) async { - const text = 'appflowy'; - const url = 'https://appflowy.io'; - await tester.pasteContent( - plainText: url, - beforeTest: (editorState) async { - await tester.ime.insertText(text); - await tester.editor.updateSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: text.length, - ), - ); - }, - (editorState) { - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ParagraphBlockKeys.type); - expect(node.delta!.toJson(), [ - { - 'insert': text, - 'attributes': {'href': url}, - } - ]); - }, - ); - }, - ); - - // https://github.com/AppFlowy-IO/AppFlowy/issues/3263 - testWidgets( - 'paste the image from clipboard when html and image are both available', - (tester) async { - const html = - '''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) { - 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); - } - }, - ); - }); - }); -} - -extension on WidgetTester { - Future pasteContent( - FutureOr Function(EditorState editorState) test, { - Future Function(EditorState editorState)? beforeTest, - String? plainText, - String? html, - String? inAppJson, - bool pasteAsPlain = false, - (String, Uint8List?)? image, - }) async { - await initializeAppFlowy(); - await tapAnonymousSignInButton(); - - // create a new document - await createNewPageWithNameUnderParent(); - // tap the editor - await tapButton(find.byType(AppFlowyEditor)); - - await beforeTest?.call(editor.getCurrentEditorState()); - - // mock the clipboard - await getIt().setData( - ClipboardServiceData( - plainText: plainText, - html: html, - inAppJson: inAppJson, - image: image, - ), - ); - - // paste the text - await simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isShiftPressed: pasteAsPlain, - isMetaPressed: Platform.isMacOS, - ); - await pumpAndSettle(const Duration(milliseconds: 1000)); - - await 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 deleted file mode 100644 index c2e00a4b48..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart +++ /dev/null @@ -1,76 +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('create and delete the document', () { - testWidgets('create a new document when launching app in first time', - (tester) async { - 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); - - // expect to see a new document - tester.expectToSeePageName(pageName); - // and with one paragraph block - expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget); - }); - - testWidgets('delete the readme page and restore it', (tester) async { - await tester.initializeAppFlowy(); - - await tester.tapAnonymousSignInButton(); - - // delete the readme page - await tester.hoverOnPageName( - gettingStarted, - onHover: () async => tester.tapDeletePageButton(), - ); - - // the banner should show up and the readme page should be gone - tester.expectToSeeDocumentBanner(); - tester.expectNotToSeePageName(gettingStarted); - - // restore the readme page - await tester.tapRestoreButton(); - - // the banner should be gone and the readme page should be back - tester.expectNotToSeeDocumentBanner(); - tester.expectToSeePageName(gettingStarted); - }); - - testWidgets('delete the readme page and delete it permanently', - (tester) async { - await tester.initializeAppFlowy(); - - await tester.tapAnonymousSignInButton(); - - // delete the readme page - await tester.hoverOnPageName( - gettingStarted, - onHover: () async => tester.tapDeletePageButton(), - ); - - // the banner should show up and the readme page should be gone - tester.expectToSeeDocumentBanner(); - tester.expectNotToSeePageName(gettingStarted); - - // delete the page permanently - await tester.tapDeletePermanentlyButton(); - - // the banner should be gone and the readme page should be gone - tester.expectNotToSeeDocumentBanner(); - tester.expectNotToSeePageName(gettingStarted); - }); - }); -} 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_page_reference_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart deleted file mode 100644 index f169910840..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart +++ /dev/null @@ -1,136 +0,0 @@ -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/home/menu/view/view_item.dart'; -import 'package:flowy_infra/uuid.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(); - - group('insert inline document reference', () { - testWidgets('insert by slash menu', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - 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); - }); - - testWidgets('insert by `[[` character shortcut', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final name = await createDocumentToReference(tester); - - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - - await tester.ime.insertText('[['); - await tester.pumpAndSettle(); - - // Select result - await tester.editor.tapAtMenuItemWithName(name); - await tester.pumpAndSettle(); - - final mentionBlock = find.byType(MentionPageBlock); - expect(mentionBlock, findsOneWidget); - }); - - testWidgets('insert by `+` character shortcut', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final name = await createDocumentToReference(tester); - - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - - await tester.ime.insertText('+'); - await tester.pumpAndSettle(); - - // Select result - await tester.editor.tapAtMenuItemWithName(name); - await tester.pumpAndSettle(); - - final mentionBlock = find.byType(MentionPageBlock); - expect(mentionBlock, findsOneWidget); - }); - }); -} - -Future createDocumentToReference(WidgetTester tester) async { - final name = 'document_${uuid()}'; - - await tester.createNewPageWithNameUnderParent( - name: name, - openAfterCreated: false, - ); - - // This is a workaround since the openAfterCreated - // option does not work in createNewPageWithName method - await tester.tap(find.byType(SingleInnerViewItem).first); - await tester.pumpAndSettle(); - - return name; -} - -Future triggerReferenceDocumentBySlashMenu(WidgetTester tester) async { - await tester.editor.showSlashMenu(); - await tester.pumpAndSettle(); - - // Search for referenced document action - await enterDocumentText(tester); - - // Select item - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - LogicalKeyboardKey.enter, - ], - tester: tester, - withKeyUp: true, - ); - - await tester.pumpAndSettle(); -} - -Future enterDocumentText(WidgetTester tester) async { - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - LogicalKeyboardKey.keyD, - LogicalKeyboardKey.keyO, - LogicalKeyboardKey.keyC, - LogicalKeyboardKey.keyU, - LogicalKeyboardKey.keyM, - LogicalKeyboardKey.keyE, - LogicalKeyboardKey.keyN, - LogicalKeyboardKey.keyT, - ], - tester: tester, - withKeyUp: true, - ); - await tester.pumpAndSettle(); -} 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 deleted file mode 100644 index eeb2ea3925..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart +++ /dev/null @@ -1,140 +0,0 @@ -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'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('MoreViewActions', () { - testWidgets('can duplicate and delete from menu', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.pumpAndSettle(); - - final pageFinder = find.byType(ViewItem); - expect(pageFinder, findsNWidgets(1)); - - // Duplicate - await tester.openMoreViewActions(); - await tester.duplicateByMoreViewActions(); - await tester.pumpAndSettle(); - - expect(pageFinder, findsNWidgets(2)); - - // Delete - await tester.openMoreViewActions(); - await tester.deleteByMoreViewActions(); - await tester.pumpAndSettle(); - - 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 deleted file mode 100644 index 6ec12287a8..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart +++ /dev/null @@ -1,178 +0,0 @@ -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'; - -import '../../shared/util.dart'; - -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''', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - var editorState = tester.editor.getCurrentEditorState(); - expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isNotEmpty); - - // add a new block after the current selection - await tester.editor.hoverAndClickOptionAddButton([0], false); - // await tester.pumpAndSettle(); - expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isEmpty); - - // cancel the selection menu - await tester.tapAt(Offset.zero); - - await tester.editor.hoverAndClickOptionAddButton([0], true); - await tester.pumpAndSettle(); - expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty); - // cancel the selection menu - await tester.tapAt(Offset.zero); - await tester.tapAt(Offset.zero); - - await tester.createNewPageWithNameUnderParent(name: 'test'); - await tester.openPage(gettingStarted); - - // check the status again - editorState = tester.editor.getCurrentEditorState(); - 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_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_text_direction_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_text_direction_test.dart deleted file mode 100644 index 2a6f730385..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_text_direction_test.dart +++ /dev/null @@ -1,57 +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('text direction', () { - testWidgets( - '''no text direction items will be displayed in the default/LTR mode, and three text direction items will be displayed when toggle is enabled.''', - (tester) async { - // combine the two tests into one to avoid the time-consuming process of initializing the app - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final selection = Selection.single( - path: [0], - startOffset: 0, - endOffset: 1, - ); - // click the first line of the readme - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.updateSelection(selection); - await tester.pumpAndSettle(); - - // because this icons are defined in the appflowy_editor package, we can't fetch the icons by SVG data. [textDirectionItems] - final textDirectionIconNames = [ - 'toolbar/text_direction_auto', - 'toolbar/text_direction_ltr', - 'toolbar/text_direction_rtl', - ]; - // no text direction items by default - var button = find.byWidgetPredicate( - (widget) => - widget is SVGIconItemWidget && - textDirectionIconNames.contains(widget.iconName), - ); - expect(button, findsNothing); - - // switch to the RTL mode - await tester.toggleEnableRTLToolbarItems(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.updateSelection(selection); - await tester.pumpAndSettle(); - - button = find.byWidgetPredicate( - (widget) => - widget is SVGIconItemWidget && - textDirectionIconNames.contains(widget.iconName), - ); - expect(button, findsNWidgets(3)); - }); - }); -} 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 deleted file mode 100644 index 84b6790403..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart +++ /dev/null @@ -1,224 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; - -import '../../shared/emoji.dart'; -import '../../shared/mock/mock_file_picker.dart'; -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('cover image:', () { - testWidgets('document cover 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 solid color background - await tester.editor.tapOnChangeCover(); - await tester.editor.switchSolidColorBackground(); - await tester.editor.dismissCoverPicker(); - tester.expectToSeeDocumentCover(CoverType.color); - - // Change cover to a network image - const imageUrl = - "https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy/main/frontend/appflowy_flutter/assets/images/appflowy_launch_splash.jpg"; - await tester.editor.hoverOnCover(); - await tester.editor.tapOnChangeCover(); - await tester.editor.addNetworkImageCover(imageUrl); - tester.expectToSeeDocumentCover(CoverType.file); - - // Remove the cover - await tester.editor.hoverOnCover(); - await tester.editor.tapOnRemoveCover(); - 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(); - - tester.expectToSeeDocumentIcon('⭐️'); - - // Insert a document icon - await tester.editor.tapGettingStartedIcon(); - await tester.tapEmoji('😀'); - tester.expectToSeeDocumentIcon('😀'); - - // Remove the document icon from the cover toolbar - await tester.editor.hoverOnCoverToolbar(); - await tester.editor.tapRemoveIconButton(); - tester.expectToSeeDocumentIcon(null); - - // Add the icon back for further testing - await tester.editor.hoverOnCoverToolbar(); - await tester.editor.tapAddIconButton(); - await tester.tapEmoji('😀'); - tester.expectToSeeDocumentIcon('😀'); - - // Change the document icon - await tester.editor.tapOnIconWidget(); - await tester.tapEmoji('😅'); - tester.expectToSeeDocumentIcon('😅'); - - // Remove the document icon from the icon picker - await tester.editor.tapOnIconWidget(); - await tester.editor.tapRemoveIconButton(isInPicker: true); - tester.expectToSeeDocumentIcon(null); - }); - - testWidgets('icon and cover at the same time', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - tester.expectToSeeDocumentIcon('⭐️'); - tester.expectToSeeNoDocumentCover(); - - // Insert a document icon - await tester.editor.tapGettingStartedIcon(); - await tester.tapEmoji('😀'); - - // Insert a document cover - await tester.editor.hoverOnCoverToolbar(); - await tester.editor.tapOnAddCover(); - - // Expect to see the icon and cover at the same time - tester.expectToSeeDocumentIcon('😀'); - tester.expectToSeeDocumentCover(CoverType.asset); - - // Hover over the cover toolbar and see that neither icons are shown - await tester.editor.hoverOnCoverToolbar(); - tester.expectToSeeEmptyDocumentHeaderToolbar(); - }); - - testWidgets('shuffle icon', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.editor.tapGettingStartedIcon(); - - // click the shuffle button - await tester.tapButton( - find.byTooltip(LocaleKeys.emoji_random.tr()), - ); - tester.expectDocumentIconNotNull(); - }); - - testWidgets('change skin tone', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.editor.tapGettingStartedIcon(); - - final searchEmojiTextField = find.byWidgetPredicate( - (widget) => - widget is TextField && - widget.decoration!.hintText == LocaleKeys.search_label.tr(), - ); - await tester.enterText( - searchEmojiTextField, - 'punch', - ); - - // change skin tone - await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark); - - // select an icon with skin tone - const punch = '👊🏿'; - await tester.tapEmoji(punch); - tester.expectToSeeDocumentIcon(punch); - tester.expectViewHasIcon( - gettingStarted, - ViewLayoutPB.Document, - EmojiIconData.emoji(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 deleted file mode 100644 index 158eb501e3..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart +++ /dev/null @@ -1,343 +0,0 @@ -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:flowy_infra/uuid.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('database view in document', () { - testWidgets('insert a referenced grid', (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); - }); - - testWidgets('insert a referenced board', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await insertLinkedDatabase(tester, ViewLayoutPB.Board); - - // validate the referenced board is inserted - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.byType(DesktopBoardPage), - ), - findsOneWidget, - ); - }); - - 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); - - // validate the referenced grid is inserted - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.byType(CalendarPage), - ), - findsOneWidget, - ); - }); - - testWidgets('create a grid inside a document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await createInlineDatabase(tester, ViewLayoutPB.Grid); - - // validate the inline grid is created - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.byType(GridPage), - ), - findsOneWidget, - ); - }); - - testWidgets('create a board inside a document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await createInlineDatabase(tester, ViewLayoutPB.Board); - - // validate the inline board is created - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.byType(DesktopBoardPage), - ), - findsOneWidget, - ); - }); - - testWidgets('create a calendar inside a document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await createInlineDatabase(tester, ViewLayoutPB.Calendar); - - // validate the inline calendar is created - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.byType(CalendarPage), - ), - 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( - WidgetTester tester, - ViewLayoutPB layout, -) async { - // create a new grid - final id = uuid(); - final name = '${layout.name}_$id'; - await tester.createNewPageWithNameUnderParent( - name: name, - layout: layout, - openAfterCreated: false, - ); - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'insert_a_reference_${layout.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( - layout.slashMenuLinkedName, - ); - - final linkToPageMenu = find.byType(InlineActionsHandler); - expect(linkToPageMenu, findsOneWidget); - final referencedDatabase = find.descendant( - of: linkToPageMenu, - matching: find.findTextInFlowyText(name), - ); - expect(referencedDatabase, findsOneWidget); - await tester.tapButton(referencedDatabase); -} - -Future createInlineDatabase( - WidgetTester tester, - ViewLayoutPB layout, -) async { - // create a new document - final documentName = 'insert_a_inline_${layout.name}'; - await tester.createNewPageWithNameUnderParent( - name: documentName, - ); - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a referenced view - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - layout.slashMenuName, - offset: 100, - ); - await tester.pumpAndSettle(); - - final childViews = tester - .widget(tester.findPageName(documentName)) - .view - .childViews; - expect(childViews.length, 1); -} 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 deleted file mode 100644 index 3dcd6be8ae..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart +++ /dev/null @@ -1,143 +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/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/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/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; - -import '../../shared/mock/mock_file_picker.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - TestWidgetsFlutterBinding.ensureInitialized(); - - group('image block in document', () { - testWidgets('insert an image from local file', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(), - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_image.tr(), - ); - expect(find.byType(CustomImageBlockComponent), findsOneWidget); - expect(find.byType(ImagePlaceholder), findsOneWidget); - expect( - find.descendant( - of: find.byType(ImagePlaceholder), - matching: find.byType(AppFlowyPopover), - ), - findsOneWidget, - ); - expect(find.byType(UploadImageMenu), findsOneWidget); - - final 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(); - }); - - testWidgets('insert two images from local file at once', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(), - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_image.tr(), - ); - expect(find.byType(CustomImageBlockComponent), findsOneWidget); - expect(find.byType(ImagePlaceholder), findsOneWidget); - expect( - find.descendant( - of: find.byType(ImagePlaceholder), - matching: find.byType(AppFlowyPopover), - ), - findsOneWidget, - ); - expect(find.byType(UploadImageMenu), findsOneWidget); - - final firstImage = - await rootBundle.load('assets/test/images/sample.jpeg'); - final secondImage = - await rootBundle.load('assets/test/images/sample.gif'); - final tempDirectory = await getTemporaryDirectory(); - - final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final firstFile = File(firstImagePath) - ..writeAsBytesSync(firstImage.buffer.asUint8List()); - - final secondImagePath = p.join(tempDirectory.path, 'sample.gif'); - final secondFile = File(secondImagePath) - ..writeAsBytesSync(secondImage.buffer.asUint8List()); - - mockPickFilePaths(paths: [firstImagePath, secondImagePath]); - - await getIt().set(KVKeys.kCloudType, '0'); - await tester.tapButtonWithName( - LocaleKeys.document_imageBlock_upload_placeholder.tr(), - ); - await tester.pumpAndSettle(); - - expect(find.byType(ResizableImage), findsNWidgets(2)); - - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - expect(firstNode.type, ImageBlockKeys.type); - expect(firstNode.attributes[ImageBlockKeys.url], isNotEmpty); - - final secondNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - expect(secondNode.type, ImageBlockKeys.type); - expect(secondNode.attributes[ImageBlockKeys.url], isNotEmpty); - - // remove the temp files - await Future.wait([firstFile.delete(), secondFile.delete()]); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart deleted file mode 100644 index 67e0149cd1..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart +++ /dev/null @@ -1,225 +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/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'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - TestWidgetsFlutterBinding.ensureInitialized(); - - group('inline math equation in document', () { - testWidgets('insert an inline math equation', (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.text( - LocaleKeys.document_toolbar_equation.tr(), - ); - await tester.tapButton(inlineMathEquationButton); - - // expect to see the math equation block - final inlineMathEquation = find.byType(InlineMathEquation); - expect(inlineMathEquation, findsOneWidget); - - // tap it and update the content - await tester.tapButton(inlineMathEquation); - final textFormField = find.descendant( - of: find.byType(MathInputTextField), - matching: find.byType(TextFormField), - ); - const newFormula = 'E = MC ^ 3'; - await tester.enterText(textFormField, newFormula); - await tester.tapButton( - find.descendant( - of: find.byType(MathInputTextField), - matching: find.byType(FlowyButton), - ), - ); - await tester.pumpAndSettle(); - }); - - testWidgets('remove the inline math equation format', (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 - var inlineMathEquation = find.byType(InlineMathEquation); - expect(inlineMathEquation, findsOneWidget); - - // highlight the math equation block - await tester.editor.updateSelection( - Selection.single(path: [0], startOffset: 0, endOffset: 1), - ); - - await tester.tapButton(moreOptionButton); - - // cancel the format - await tester.tapButton(inlineMathEquationButton); - - // expect to see the math equation block is removed - inlineMathEquation = find.byType(InlineMathEquation); - expect(inlineMathEquation, findsNothing); - - 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_math_equation_test_1.png b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_1.png deleted file mode 100644 index 8795110cf9..0000000000 Binary files a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_1.png and /dev/null differ diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_2.png b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_2.png deleted file mode 100644 index de3d4f6113..0000000000 Binary files a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test_2.png and /dev/null differ 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 deleted file mode 100644 index 12047bd37f..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart +++ /dev/null @@ -1,141 +0,0 @@ -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:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('inline page view in document', () { - testWidgets('insert a inline page - grid', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await insertInlinePage(tester, ViewLayoutPB.Grid); - - final mentionBlock = find.byType(MentionPageBlock); - expect(mentionBlock, findsOneWidget); - await tester.tapButton(mentionBlock); - }); - - testWidgets('insert a inline page - board', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await insertInlinePage(tester, ViewLayoutPB.Board); - - final mentionBlock = find.byType(MentionPageBlock); - expect(mentionBlock, findsOneWidget); - await tester.tapButton(mentionBlock); - }); - - testWidgets('insert a inline page - calendar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await insertInlinePage(tester, ViewLayoutPB.Calendar); - - final mentionBlock = find.byType(MentionPageBlock); - expect(mentionBlock, findsOneWidget); - await tester.tapButton(mentionBlock); - }); - - testWidgets('insert a inline page - document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await insertInlinePage(tester, ViewLayoutPB.Document); - - final mentionBlock = find.byType(MentionPageBlock); - expect(mentionBlock, findsOneWidget); - await tester.tapButton(mentionBlock); - }); - - testWidgets('insert a inline page and rename it', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final pageName = await insertInlinePage(tester, ViewLayoutPB.Document); - - // rename - const newName = 'RenameToNewPageName'; - await tester.hoverOnPageName( - pageName, - onHover: () async => tester.renamePage(newName), - ); - final finder = find.descendant( - of: find.byType(MentionPageBlock), - matching: find.findTextInFlowyText(newName), - ); - expect(finder, findsOneWidget); - }); - - testWidgets('insert a inline page and delete it', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final pageName = await insertInlinePage(tester, ViewLayoutPB.Grid); - - // rename - await tester.hoverOnPageName( - pageName, - layout: ViewLayoutPB.Grid, - onHover: () async => tester.tapDeletePageButton(), - ); - final finder = find.descendant( - of: find.byType(MentionPageBlock), - matching: find.findTextInFlowyText(pageName), - ); - 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); - }); - }); -} - -/// Insert a referenced database of [layout] into the document -Future insertInlinePage( - WidgetTester tester, - ViewLayoutPB layout, -) async { - // create a new grid - final id = uuid(); - final name = '${layout.name}_$id'; - await tester.createNewPageWithNameUnderParent( - name: name, - layout: layout, - openAfterCreated: false, - ); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'insert_a_inline_page_${layout.name}', - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - - // insert a inline page - await tester.editor.showAtMenu(); - await tester.editor.tapAtMenuItemWithName(name); - - return name; -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_link_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_link_test.dart deleted file mode 100644 index c369af9002..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_link_test.dart +++ /dev/null @@ -1,82 +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 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('test editing link in document', () { - late MockUrlLauncher mock; - - setUp(() { - mock = MockUrlLauncher(); - UrlLauncherPlatform.instance = mock; - }); - - testWidgets('insert/edit/open link', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a inline page - const link = 'AppFlowy'; - await tester.ime.insertText(link); - await tester.editor.updateSelection( - Selection.single(path: [0], startOffset: 0, endOffset: link.length), - ); - - // tap the link button - final linkButton = find.byTooltip( - 'Link', - ); - await tester.tapButton(linkButton); - expect(find.text('Add your link', findRichText: true), findsOneWidget); - - // input the link - const url = 'https://appflowy.io'; - final textField = find.byWidgetPredicate( - (widget) => widget is TextField && widget.decoration!.hintText == 'URL', - ); - await tester.enterText(textField, url); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - // single-click the link menu to show the menu - await tester.tapButton(find.text(link, findRichText: true)); - expect(find.text('Open link', findRichText: true), findsOneWidget); - expect(find.text('Copy link', findRichText: true), findsOneWidget); - expect(find.text('Remove link', findRichText: true), findsOneWidget); - - // double-click the link menu to open the link - mock - ..setLaunchExpectations( - url: url, - useSafariVC: false, - useWebView: false, - universalLinksOnly: false, - enableJavaScript: true, - enableDomStorage: true, - headers: {}, - webOnlyWindowName: null, - launchMode: PreferredLaunchMode.platformDefault, - ) - ..setResponse(true); - - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.doubleTapAt( - tester.getTopLeft(find.text(link, findRichText: true)).translate(5, 5), - ); - expect(mock.canLaunchCalled, isTrue); - expect(mock.launchCalled, isTrue); - }); - }); -} 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 deleted file mode 100644 index 2f3f8c80b9..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/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/util.dart'; - -const String heading1 = "Heading 1"; -const String heading2 = "Heading 2"; -const String heading3 = "Heading 3"; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('outline block test', () { - testWidgets('insert an outline block', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - name: 'outline_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await insertOutlineInDocument(tester); - - // validate the outline is inserted - expect(find.byType(OutlineBlockWidget), findsOneWidget); - }); - - testWidgets('insert an outline block and check if headings are visible', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - name: 'outline_test', - ); - - await insertHeadingComponent(tester); - /* Results in: - * # Heading 1 - * ## Heading 2 - * ### Heading 3 - * > # Heading 1 - * > ## Heading 2 - * > ### Heading 3 - */ - - await tester.editor.tapLineOfEditorAt(3); - await insertOutlineInDocument(tester); - - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading1), - ), - findsNWidgets(2), - ); - - // Heading 2 is prefixed with a bullet - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading2), - ), - findsNWidgets(2), - ); - - // Heading 3 is prefixed with a dash - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading3), - ), - findsNWidgets(2), - ); - - // update the Heading 1 to Heading 1Hello world - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('Hello world'); - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text('${heading1}Hello world'), - ), - findsOneWidget, - ); - }); - - testWidgets("control the depth of outline block", (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - name: 'outline_test', - ); - - await insertHeadingComponent(tester); - /* Results in: - * # Heading 1 - * ## Heading 2 - * ### Heading 3 - * > # Heading 1 - * > ## Heading 2 - * > ### Heading 3 - */ - - await tester.editor.tapLineOfEditorAt(7); - await insertOutlineInDocument(tester); - - // expect to find only the `heading1` widget under the [OutlineBlockWidget] - await hoverAndClickDepthOptionAction(tester, [6], 1); - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading2), - ), - findsNothing, - ); - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading3), - ), - findsNothing, - ); - ////// - - /// expect to find only the 'heading1' and 'heading2' under the [OutlineBlockWidget] - await hoverAndClickDepthOptionAction(tester, [6], 2); - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading3), - ), - findsNothing, - ); - ////// - - // expect to find all the headings under the [OutlineBlockWidget] - await hoverAndClickDepthOptionAction(tester, [6], 3); - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading1), - ), - findsNWidgets(2), - ); - - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading2), - ), - findsNWidgets(2), - ); - - expect( - find.descendant( - of: find.byType(OutlineBlockWidget), - matching: find.text(heading3), - ), - findsNWidgets(2), - ); - ////// - }); - }); -} - -/// Inserts an outline block in the document -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(), - ); - await tester.pumpAndSettle(); -} - -Future hoverAndClickDepthOptionAction( - WidgetTester tester, - List path, - int level, -) async { - await tester.editor.openDepthMenu(path); - final type = OptionDepthType.fromLevel(level); - await tester.tapButton(find.findTextInFlowyText(type.description)); - 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 deleted file mode 100644 index a4d011dccb..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart +++ /dev/null @@ -1,288 +0,0 @@ -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'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - TestWidgetsFlutterBinding.ensureInitialized(); - - group('toggle list in document', () { - Finder findToggleListIcon({ - required bool isExpanded, - }) { - final turns = isExpanded ? 0.25 : 0.0; - return find.byWidgetPredicate( - (widget) => widget is AnimatedRotation && widget.turns == turns, - ); - } - - void expectToggleListOpened() { - expect(findToggleListIcon(isExpanded: true), findsOneWidget); - expect(findToggleListIcon(isExpanded: false), findsNothing); - } - - void expectToggleListClosed() { - expect(findToggleListIcon(isExpanded: false), findsOneWidget); - expect(findToggleListIcon(isExpanded: true), findsNothing); - } - - testWidgets('convert > to toggle list, and click the icon to close it', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a toggle list - const text = 'This is a toggle list sample'; - await tester.ime.insertText('> $text'); - - final editorState = tester.editor.getCurrentEditorState(); - final toggleList = editorState.document.nodeAtPath([0])!; - expect( - toggleList.type, - ToggleListBlockKeys.type, - ); - expect( - toggleList.attributes[ToggleListBlockKeys.collapsed], - false, - ); - expect( - toggleList.delta!.toPlainText(), - text, - ); - - // Simulate pressing enter key to move the cursor to the next line - await tester.ime.insertCharacter('\n'); - const text2 = 'This is a child node'; - await tester.ime.insertText(text2); - expect(find.text(text2, findRichText: true), findsOneWidget); - - // Click the toggle list icon to close it - final toggleListIcon = find.byIcon(Icons.arrow_right); - await tester.tapButton(toggleListIcon); - - // expect the toggle list to be closed - expect(find.text(text2, findRichText: true), findsNothing); - }); - - testWidgets('press enter key when the toggle list is closed', - (tester) async { - // if the toggle list is closed, press enter key will insert a new toggle list after it - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a toggle list - const text = 'Hello AppFlowy'; - await tester.ime.insertText('> $text'); - - // Click the toggle list icon to close it - final toggleListIcon = find.byIcon(Icons.arrow_right); - await tester.tapButton(toggleListIcon); - - // Press the enter key - await tester.editor.updateSelection( - Selection.collapsed( - Position(path: [0], offset: 'Hello '.length), - ), - ); - await tester.ime.insertCharacter('\n'); - - final editorState = tester.editor.getCurrentEditorState(); - final node0 = editorState.getNodeAtPath([0])!; - final node1 = editorState.getNodeAtPath([1])!; - - expect(node0.type, ToggleListBlockKeys.type); - expect(node0.attributes[ToggleListBlockKeys.collapsed], true); - expect(node0.delta!.toPlainText(), 'Hello '); - expect(node1.type, ToggleListBlockKeys.type); - expect(node1.delta!.toPlainText(), 'AppFlowy'); - }); - - testWidgets('press enter key when the toggle list is open', (tester) async { - // if the toggle list is open, press enter key will insert a new paragraph inside it - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a toggle list - const text = 'Hello AppFlowy'; - await tester.ime.insertText('> $text'); - - // Press the enter key - await tester.editor.updateSelection( - Selection.collapsed( - Position(path: [0], offset: 'Hello '.length), - ), - ); - await tester.ime.insertCharacter('\n'); - - final editorState = tester.editor.getCurrentEditorState(); - final node0 = editorState.getNodeAtPath([0])!; - final node00 = editorState.getNodeAtPath([0, 0])!; - final node1 = editorState.getNodeAtPath([1]); - - expect(node0.type, ToggleListBlockKeys.type); - expect(node0.attributes[ToggleListBlockKeys.collapsed], false); - expect(node0.delta!.toPlainText(), 'Hello '); - expect(node00.type, ParagraphBlockKeys.type); - expect(node00.delta!.toPlainText(), 'AppFlowy'); - expect(node1, isNull); - }); - - testWidgets('clear the format if toggle list if empty', (tester) async { - // if the toggle list is open, press enter key will insert a new paragraph inside it - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a toggle list - await tester.ime.insertText('> '); - - // Press the enter key - // Click the toggle list icon to close it - final toggleListIcon = find.byIcon(Icons.arrow_right); - await tester.tapButton(toggleListIcon); - - await tester.editor - .updateSelection(Selection.collapsed(Position(path: [0]))); - await tester.ime.insertCharacter('\n'); - - final editorState = tester.editor.getCurrentEditorState(); - final node0 = editorState.getNodeAtPath([0])!; - - expect(node0.type, ParagraphBlockKeys.type); - }); - - testWidgets('use cmd/ctrl + enter to open/close the toggle list', - (tester) async { - // if the toggle list is open, press enter key will insert a new paragraph inside it - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a toggle list - await tester.ime.insertText('> Hello'); - - expectToggleListOpened(); - - await tester.editor.updateSelection( - Selection.collapsed( - Position(path: [0]), - ), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.enter, - isMetaPressed: Platform.isMacOS, - isControlPressed: Platform.isLinux || Platform.isWindows, - ); - - expectToggleListClosed(); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.enter, - isMetaPressed: Platform.isMacOS, - isControlPressed: Platform.isLinux || Platform.isWindows, - ); - - 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/document/edit_document_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.dart deleted file mode 100644 index e4804a2a57..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'dart:io'; - -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 '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('edit document', () { - testWidgets('redo & undo', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document called Sample - const pageName = 'Sample'; - await tester.createNewPageWithNameUnderParent(name: pageName); - - // focus on the editor - await tester.editor.tapLineOfEditorAt(0); - - // insert 1. to trigger it to be a numbered list - await tester.ime.insertText('1. '); - expect(find.text('1.', findRichText: true), findsOneWidget); - expect( - tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, - NumberedListBlockKeys.type, - ); - - // undo - // numbered list will be reverted to paragraph - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: Platform.isWindows || Platform.isLinux, - isMetaPressed: Platform.isMacOS, - ); - expect( - tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, - ParagraphBlockKeys.type, - ); - - // redo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: Platform.isWindows || Platform.isLinux, - isMetaPressed: Platform.isMacOS, - isShiftPressed: true, - ); - expect( - tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, - NumberedListBlockKeys.type, - ); - - // switch to other page and switch back - await tester.openPage(gettingStarted); - await tester.openPage(pageName); - - // the numbered list should be kept - expect( - tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, - NumberedListBlockKeys.type, - ); - }); - - testWidgets('write a readme document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document called Sample - const pageName = 'Sample'; - await tester.createNewPageWithNameUnderParent(name: pageName); - - // focus on the editor - await tester.editor.tapLineOfEditorAt(0); - - // mock inputting the sample - final lines = _sample.split('\n'); - for (final line in lines) { - await tester.ime.insertText(line); - await tester.ime.insertCharacter('\n'); - } - - // switch to other page and switch back - await tester.openPage(gettingStarted); - await tester.openPage(pageName); - - // this screenshots are different on different platform, so comment it out temporarily. - // check the document - // await expectLater( - // find.byType(AppFlowyEditor), - // matchesGoldenFile('document/edit_document_test.png'), - // ); - }); - }); -} - -const _sample = ''' -# Heading 1 -## Heading 2 -### Heading 3 ---- -[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~ - -[] Type followed by bullet or num to create a list. - -[x] Click `New Page` button at the bottom of your sidebar to add a new page. - -[] Click the plus sign next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. ---- -* bulleted list 1 - -* bulleted list 2 - -* bulleted list 3 -bulleted list 4 ---- -1. numbered list 1 - -2. numbered list 2 - -3. numbered list 3 -numbered list 4 ---- -" quote'''; 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_calculations_test.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_calculations_test.dart deleted file mode 100644 index 2eaa7ea6a5..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_calculations_test.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.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() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Grid Calculations', () { - testWidgets('add calculation and update cell', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Change one Field to Number - await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number); - - expect(find.text('Calculate'), findsOneWidget); - - await tester.changeCalculateAtIndex(1, CalculationType.Sum); - - // Enter values in cells - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.Number, - input: '100', - ); - - await tester.editCell( - rowIndex: 1, - fieldType: FieldType.Number, - input: '100', - ); - - // Dismiss edit cell - await tester.sendKeyDownEvent(LogicalKeyboardKey.enter); - - await tester.pumpAndSettle(const Duration(seconds: 1)); - - expect(find.text('200'), findsOneWidget); - }); - - testWidgets('add calculations and remove row', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Change two Fields to Number - await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number); - await tester.changeFieldTypeOfFieldWithName('Done', FieldType.Number); - - expect(find.text('Calculate'), findsNWidgets(2)); - - await tester.changeCalculateAtIndex(1, CalculationType.Sum); - await tester.changeCalculateAtIndex(2, CalculationType.Min); - - // Enter values in cells - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.Number, - input: '100', - ); - await tester.editCell( - rowIndex: 1, - fieldType: FieldType.Number, - input: '150', - ); - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.Number, - input: '50', - cellIndex: 1, - ); - await tester.editCell( - rowIndex: 1, - fieldType: FieldType.Number, - input: '100', - cellIndex: 1, - ); - - await tester.pumpAndSettle(); - - // Dismiss edit cell - await tester.sendKeyDownEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - expect(find.text('250'), findsOneWidget); - expect(find.text('50'), findsNWidgets(2)); - - // Delete 1st row - await tester.hoverOnFirstRowOfGrid(); - await tester.tapRowMenuButtonInGrid(); - await tester.tapDeleteOnRowMenu(); - - await tester.pumpAndSettle(const Duration(seconds: 1)); - - expect(find.text('150'), findsNWidgets(2)); - expect(find.text('100'), findsNWidgets(2)); - }); - }); -} 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 deleted file mode 100644 index bdfe2dae9f..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart +++ /dev/null @@ -1,168 +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/user/application/user_settings_service.dart'; -import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.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_ui/style_widget/text_field.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/base.dart'; -import '../../shared/common_operations.dart'; -import '../../shared/document_test_operations.dart'; -import '../../shared/expectation.dart'; -import '../../shared/keyboard.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Reminder in Document', () { - testWidgets('Add reminder for tomorrow, and include time', (tester) async { - const time = "23:59"; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final dateTimeSettings = - await UserSettingsBackendService().getDateTimeSettings(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.getCurrentEditorState().insertNewLine(); - - await tester.pumpAndSettle(); - - // Trigger inline action menu and type 'remind tomorrow' - final tomorrow = await _insertReminderTomorrow(tester); - - Node node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!; - Map mentionAttr = - node.delta!.first.attributes![MentionBlockKeys.mention]; - - expect(node.type, 'paragraph'); - expect(mentionAttr['type'], MentionType.date.name); - expect(mentionAttr['date'], tomorrow.toIso8601String()); - - await tester.tap( - find.text(dateTimeSettings.dateFormat.formatDate(tomorrow, false)), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(Toggle)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byType(FlowyTextField), time); - - // Leave text field to submit - await tester.tap(find.text(LocaleKeys.grid_field_includeTime.tr())); - await tester.pumpAndSettle(); - - node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!; - mentionAttr = node.delta!.first.attributes![MentionBlockKeys.mention]; - - final tomorrowWithTime = - _dateWithTime(dateTimeSettings.timeFormat, tomorrow, time); - - expect(node.type, 'paragraph'); - expect(mentionAttr['type'], MentionType.date.name); - expect(mentionAttr['date'], tomorrowWithTime.toIso8601String()); - }); - - testWidgets('Add reminder for tomorrow, and navigate to it', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.getCurrentEditorState().insertNewLine(); - - await tester.pumpAndSettle(); - - // Trigger inline action menu and type 'remind tomorrow' - final tomorrow = await _insertReminderTomorrow(tester); - - final Node node = - tester.editor.getCurrentEditorState().getNodeAtPath([1])!; - final Map mentionAttr = - node.delta!.first.attributes![MentionBlockKeys.mention]; - - expect(node.type, 'paragraph'); - expect(mentionAttr['type'], MentionType.date.name); - expect(mentionAttr['date'], tomorrow.toIso8601String()); - - // Create and Navigate to a new document - await tester.createNewPageWithNameUnderParent(); - await tester.pumpAndSettle(); - - // Open "Upcoming" in Notification hub - await tester.openNotificationHub(tabIndex: 1); - - // Expect 1 notification - tester.expectNotificationItems(1); - - // Tap on the notification - await tester.tap(find.byType(NotificationItem)); - await tester.pumpAndSettle(); - - // Expect node at path 1 to be the date/reminder - expect( - tester.editor - .getCurrentEditorState() - .getNodeAtPath([1]) - ?.delta - ?.first - .attributes?[MentionBlockKeys.mention]['type'], - MentionType.date.name, - ); - }); - }); -} - -Future _insertReminderTomorrow(WidgetTester tester) async { - await tester.editor.showAtMenu(); - - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - LogicalKeyboardKey.keyR, - LogicalKeyboardKey.keyE, - LogicalKeyboardKey.keyM, - LogicalKeyboardKey.keyI, - LogicalKeyboardKey.keyN, - LogicalKeyboardKey.keyD, - LogicalKeyboardKey.space, - LogicalKeyboardKey.keyT, - LogicalKeyboardKey.keyO, - LogicalKeyboardKey.keyM, - LogicalKeyboardKey.keyO, - LogicalKeyboardKey.keyR, - LogicalKeyboardKey.keyR, - LogicalKeyboardKey.keyO, - LogicalKeyboardKey.keyW, - ], - tester: tester, - ); - - await FlowyTestKeyboard.simulateKeyDownEvent( - [LogicalKeyboardKey.enter], - tester: tester, - ); - - return DateTime.now().add(const Duration(days: 1)).withoutTime; -} - -DateTime _dateWithTime(UserTimeFormatPB format, DateTime date, String time) { - final t = format == UserTimeFormatPB.TwelveHour - ? DateFormat.jm().parse(time) - : DateFormat.Hm().parse(time); - - return DateTime.parse( - '${date.year}${_padZeroLeft(date.month)}${_padZeroLeft(date.day)} ${_padZeroLeft(t.hour)}:${_padZeroLeft(t.minute)}', - ); -} - -String _padZeroLeft(int a) => a.toString().padLeft(2, '0'); 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 deleted file mode 100644 index 570c482fb5..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('notification test', () { - testWidgets('enable notification', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.notifications); - await tester.pumpAndSettle(); - - final toggleFinder = find.byType(Toggle).first; - - // Defaults to enabled - Toggle toggleWidget = tester.widget(toggleFinder); - expect(toggleWidget.value, true); - - // Disable - await tester.tap(toggleFinder); - await tester.pumpAndSettle(); - - toggleWidget = tester.widget(toggleFinder); - expect(toggleWidget.value, false); - - // Enable again - await tester.tap(toggleFinder); - await tester.pumpAndSettle(); - - toggleWidget = tester.widget(toggleFinder); - expect(toggleWidget.value, true); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart deleted file mode 100644 index a311eb8377..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/auth_operation.dart'; -import '../../shared/base.dart'; -import '../../shared/expectation.dart'; -import '../../shared/settings.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Settings Billing', () { - testWidgets('Local auth cannot see plan+billing', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapSignInAsGuest(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.openSettings(); - await tester.pumpAndSettle(); - - // We check that another settings page is present to ensure - // it's not a fluke - expect( - find.text( - LocaleKeys.settings_workspacePage_menuLabel.tr(), - skipOffstage: false, - ), - findsOneWidget, - ); - - expect( - find.text( - LocaleKeys.settings_planPage_menuLabel.tr(), - skipOffstage: false, - ), - findsNothing, - ); - - expect( - find.text( - LocaleKeys.settings_billingPage_menuLabel.tr(), - skipOffstage: false, - ), - findsNothing, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart deleted file mode 100644 index 617d495265..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart +++ /dev/null @@ -1,15 +0,0 @@ -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(); - - 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 deleted file mode 100644 index fe91becba6..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart +++ /dev/null @@ -1,94 +0,0 @@ -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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('shortcuts:', () { - testWidgets('change and overwrite shortcut', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.shortcuts); - await tester.pumpAndSettle(); - - final backspaceCmd = - 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.pumpAndSettle(); - - await tester.hoverOnWidget( - find - .descendant( - of: find.byType(ShortcutSettingTile), - matching: find.text(backspaceCmd), - ) - .first, - onHover: () async { - await tester.tap(find.byFlowySvg(FlowySvgs.edit_s)); - await tester.pumpAndSettle(); - - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - LogicalKeyboardKey.delete, - LogicalKeyboardKey.enter, - ], - tester: tester, - ); - await tester.pumpAndSettle(); - }, - ); - - // We expect to see conflict dialog - expect( - find.text( - LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(), - ), - findsOneWidget, - ); - - // Press on confirm label - await tester.tap( - find.text( - LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(), - ), - ); - await tester.pumpAndSettle(); - - // We expect the first ShortcutSettingTile to have one - // [KeyBadge] with `delete` label - final first = tester.widget(find.byType(ShortcutSettingTile).first) - as ShortcutSettingTile; - expect( - first.command.command, - 'delete', - ); - - // And the second one which is `Delete left character` to have none - // as it will have been overwritten - final second = tester.widget(find.byType(ShortcutSettingTile).at(1)) - as ShortcutSettingTile; - expect( - second.command.command, - '', - ); - }); - }); -} 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/rename_current_item_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/rename_current_item_test.dart deleted file mode 100644 index 8780465c32..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/rename_current_item_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/keyboard.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Rename current view item', () { - testWidgets('by F2 shortcut', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await FlowyTestKeyboard.simulateKeyDownEvent( - [LogicalKeyboardKey.f2], - tester: tester, - ); - await tester.pumpAndSettle(); - - expect(find.byType(RenameViewPopover), findsOneWidget); - - await tester.enterText( - find.descendant( - of: find.byType(RenameViewPopover), - matching: find.byType(FlowyTextField), - ), - 'hello', - ); - await tester.pumpAndSettle(); - - // Dismiss rename popover - await tester.tap(find.byType(AppFlowyEditor)); - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(SingleInnerViewItem), - matching: find.text('hello'), - ), - 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 deleted file mode 100644 index ad18cf3de6..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart +++ /dev/null @@ -1,129 +0,0 @@ -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'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('sidebar expand test', () { - bool isExpanded({required FolderSpaceType type}) { - if (type == FolderSpaceType.private) { - return find - .descendant( - of: find.byType(PrivateSectionFolder), - matching: find.byType(ViewItem), - ) - .evaluate() - .isNotEmpty; - } - return false; - } - - testWidgets('first time the personal folder is expanded', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // first time is expanded - expect(isExpanded(type: FolderSpaceType.private), true); - - // collapse the personal folder - await tester.tapButton( - find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), - ); - expect(isExpanded(type: FolderSpaceType.private), false); - - // expand the personal folder - await tester.tapButton( - find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), - ); - 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 deleted file mode 100644 index 3345ed30ab..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/style_widget/hover.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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Favorites', () { - testWidgets( - 'Toggle favorites for views creates / removes the favorite header along with favorite views', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // no favorite folder - expect(find.byType(FavoriteFolder), findsNothing); - - // create the nested views - final names = [ - 1, - 2, - ].map((e) => 'document_$e').toList(); - for (var i = 0; i < names.length; i++) { - final parentName = i == 0 ? gettingStarted : names[i - 1]; - await tester.createNewPageWithNameUnderParent( - name: names[i], - parentName: parentName, - ); - tester.expectToSeePageName(names[i], parentName: parentName); - } - - await tester.favoriteViewByName(gettingStarted); - expect( - tester.findFavoritePageName(gettingStarted), - findsOneWidget, - ); - - await tester.favoriteViewByName(names[1]); - expect( - tester.findFavoritePageName(names[1]), - findsNWidgets(1), - ); - - await tester.unfavoriteViewByName(gettingStarted); - expect( - tester.findFavoritePageName(gettingStarted), - findsNothing, - ); - expect( - tester.findFavoritePageName( - names[1], - ), - findsOneWidget, - ); - - await tester.unfavoriteViewByName(names[1]); - expect( - tester.findFavoritePageName( - names[1], - ), - findsNothing, - ); - }); - - testWidgets( - 'renaming a favorite view updates name under favorite header', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const name = 'test'; - await tester.favoriteViewByName(gettingStarted); - await tester.hoverOnPageName( - gettingStarted, - onHover: () async { - await tester.renamePage(name); - await tester.pumpAndSettle(); - }, - ); - expect( - tester.findPageName(name), - findsNWidgets(2), - ); - expect( - tester.findFavoritePageName(name), - findsOneWidget, - ); - }, - ); - - testWidgets( - 'deleting first level favorite view removes its instance from favorite header, deleting root level views leads to removal of all favorites that are its children', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final names = [1, 2].map((e) => 'document_$e').toList(); - for (var i = 0; i < names.length; i++) { - final parentName = i == 0 ? gettingStarted : names[i - 1]; - await tester.createNewPageWithNameUnderParent( - name: names[i], - parentName: parentName, - ); - tester.expectToSeePageName(names[i], parentName: parentName); - } - await tester.favoriteViewByName(gettingStarted); - await tester.favoriteViewByName(names[0]); - await tester.favoriteViewByName(names[1]); - - expect( - find.byWidgetPredicate( - (widget) => - widget is SingleInnerViewItem && - widget.view.isFavorite && - widget.spaceType == FolderSpaceType.favorite, - ), - findsNWidgets(3), - ); - - await tester.hoverOnPageName( - names[1], - onHover: () async { - await tester.tapDeletePageButton(); - await tester.pumpAndSettle(); - }, - ); - - expect( - tester.findAllFavoritePages(), - findsNWidgets(2), - ); - - await tester.hoverOnPageName( - gettingStarted, - onHover: () async { - await tester.tapDeletePageButton(); - await tester.pumpAndSettle(); - }, - ); - - expect( - tester.findAllFavoritePages(), - findsNothing, - ); - }, - ); - - testWidgets( - 'view selection is synced between favorites and personal folder', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - await tester.favoriteViewByName(gettingStarted); - expect( - find.byWidgetPredicate( - (widget) => - widget is FlowyHover && - widget.isSelected != null && - widget.isSelected!(), - ), - findsNWidgets(1), - ); - }, - ); - - testWidgets( - 'context menu opens up for favorites', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - await tester.favoriteViewByName(gettingStarted); - await tester.hoverOnPageName( - gettingStarted, - useLast: false, - onHover: () async { - await tester.tapPageOptionButton(); - await tester.pumpAndSettle(); - expect( - find.byType(PopoverContainer), - findsOneWidget, - ); - }, - ); - 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 deleted file mode 100644 index 2236f03960..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart +++ /dev/null @@ -1,346 +0,0 @@ -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('😁'); - - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - 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, - ); - - // update its emoji - await tester.updatePageIconInSidebarByName( - name: value.name, - parentName: gettingStarted, - layout: value, - icon: emoji, - ); - - tester.expectViewHasIcon( - value.name, - value, - emoji, - ); - } - }); - - testWidgets('Update page emoji in title bar', (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, - ); - - // 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 deleted file mode 100644 index 304e8e2e35..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart +++ /dev/null @@ -1,206 +0,0 @@ -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/workspace/presentation/home/menu/view/draggable_view_item.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_backend/protobuf/flowy-folder/view.pb.dart'; -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('sidebar:', () { - testWidgets('create a new page', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new page - await tester.tapNewPageButton(); - - // expect to see a new document - tester.expectToSeePageName(''); - // and with one paragraph block - expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget); - }); - - testWidgets('create a new document, grid, board and calendar', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - for (final layout in ViewLayoutPB.values) { - if (layout == ViewLayoutPB.Chat) { - continue; - } - // create a new page - final name = 'AppFlowy_$layout'; - await tester.createNewPageWithNameUnderParent( - name: name, - layout: layout, - ); - - // expect to see a new page - tester.expectToSeePageName( - name, - layout: layout, - ); - - switch (layout) { - case ViewLayoutPB.Document: - // and with one paragraph block - expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget); - break; - case ViewLayoutPB.Grid: - expect(find.byType(GridPage), findsOneWidget); - break; - case ViewLayoutPB.Board: - expect(find.byType(DesktopBoardPage), findsOneWidget); - break; - case ViewLayoutPB.Calendar: - expect(find.byType(CalendarPage), findsOneWidget); - break; - case ViewLayoutPB.Chat: - break; - } - - await tester.openPage(gettingStarted); - } - }); - - testWidgets('create some nested pages, and move them', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final names = [1, 2, 3, 4].map((e) => 'document_$e').toList(); - for (var i = 0; i < names.length; i++) { - final parentName = i == 0 ? gettingStarted : names[i - 1]; - await tester.createNewPageWithNameUnderParent( - name: names[i], - parentName: parentName, - ); - tester.expectToSeePageName(names[i], parentName: parentName); - } - - // move the document_3 to the getting started page - await tester.movePageToOtherPage( - name: names[3], - parentName: gettingStarted, - layout: ViewLayoutPB.Document, - parentLayout: ViewLayoutPB.Document, - ); - final fromId = tester - .widget(tester.findPageName(names[3])) - .view - .parentViewId; - final toId = tester - .widget(tester.findPageName(gettingStarted)) - .view - .id; - expect(fromId, toId); - - // move the document_2 before document_1 - await tester.movePageToOtherPage( - name: names[2], - parentName: gettingStarted, - layout: ViewLayoutPB.Document, - parentLayout: ViewLayoutPB.Document, - position: DraggableHoverPosition.bottom, - ); - final childViews = tester - .widget(tester.findPageName(gettingStarted)) - .view - .childViews; - expect( - childViews[0].id, - tester - .widget(tester.findPageName(names[2])) - .view - .id, - ); - expect( - childViews[1].id, - tester - .widget(tester.findPageName(names[0])) - .view - .id, - ); - expect( - childViews[2].id, - tester - .widget(tester.findPageName(names[3])) - .view - .id, - ); - }); - - testWidgets('unable to move a document into a database', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const document = 'document'; - await tester.createNewPageWithNameUnderParent( - name: document, - openAfterCreated: false, - ); - tester.expectToSeePageName(document); - - const grid = 'grid'; - await tester.createNewPageWithNameUnderParent( - name: grid, - layout: ViewLayoutPB.Grid, - openAfterCreated: false, - ); - tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid); - - // move the document to the grid page - await tester.movePageToOtherPage( - name: document, - parentName: grid, - layout: ViewLayoutPB.Document, - parentLayout: ViewLayoutPB.Grid, - ); - - // it should not be moved - final childViews = tester - .widget(tester.findPageName(gettingStarted)) - .view - .childViews; - expect( - childViews[0].name, - document, - ); - expect( - childViews[1].name, - grid, - ); - }); - - testWidgets('unable to create a new database inside the existing one', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const grid = 'grid'; - await tester.createNewPageWithNameUnderParent( - name: grid, - layout: ViewLayoutPB.Grid, - ); - tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid); - - await tester.hoverOnPageName( - grid, - layout: ViewLayoutPB.Grid, - onHover: () async { - expect(find.byType(ViewAddButton), findsNothing); - expect(find.byType(ViewMoreActionPopover), 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 deleted file mode 100644 index ef7d3dbc8b..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart +++ /dev/null @@ -1,19 +0,0 @@ -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(); - - // Sidebar integration tests - sidebar_test.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 deleted file mode 100644 index d3226a3ad0..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart +++ /dev/null @@ -1,166 +0,0 @@ -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: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 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); - - expect(find.byType(EmojiHandler), findsNothing); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyE, - isAltPressed: true, - isMetaPressed: Platform.isMacOS, - isControlPressed: !Platform.isMacOS, - ); - 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); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart deleted file mode 100644 index 4a38dde920..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart +++ /dev/null @@ -1,100 +0,0 @@ -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_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/keyboard.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('hotkeys test', () { - testWidgets('toggle theme mode', (tester) async { - await tester.initializeAppFlowy(); - - await tester.tapAnonymousSignInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.workspace); - await tester.pumpAndSettle(); - - final appFinder = find.byType(MaterialApp).first; - ThemeMode? themeMode = tester.widget(appFinder).themeMode; - - expect(themeMode, ThemeMode.system); - - await tester.tapButton( - find.bySemanticsLabel( - LocaleKeys.settings_workspacePage_appearance_options_light.tr(), - ), - ); - await tester.pumpAndSettle(); - - themeMode = tester.widget(appFinder).themeMode; - expect(themeMode, ThemeMode.light); - - await tester.tapButton( - find.bySemanticsLabel( - LocaleKeys.settings_workspacePage_appearance_options_dark.tr(), - ), - ); - await tester.pumpAndSettle(); - - themeMode = tester.widget(appFinder).themeMode; - expect(themeMode, ThemeMode.dark); - - await tester.tap(find.byType(SettingsDialog)); - await tester.pumpAndSettle(); - - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - Platform.isMacOS - ? LogicalKeyboardKey.meta - : LogicalKeyboardKey.control, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyL, - ], - tester: tester, - ); - await tester.pumpAndSettle(); - - themeMode = tester.widget(appFinder).themeMode; - expect(themeMode, ThemeMode.light); - }); - - testWidgets('show or hide home menu', (tester) async { - await tester.initializeAppFlowy(); - - await tester.tapAnonymousSignInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.pumpAndSettle(); - - expect(find.byType(HomeSideBar), findsOneWidget); - - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - Platform.isMacOS - ? LogicalKeyboardKey.meta - : LogicalKeyboardKey.control, - LogicalKeyboardKey.backslash, - ], - tester: tester, - ); - - await tester.pumpAndSettle(); - - expect(find.byType(HomeSideBar), findsNothing); - }); - }); -} 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 deleted file mode 100644 index 84da89f6b7..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart +++ /dev/null @@ -1,95 +0,0 @@ -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'; -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('import files', () { - testWidgets('import multiple markdown files', (tester) async { - final context = await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // expect to see a getting started page - tester.expectToSeePageName(gettingStarted); - - await tester.tapAddViewButton(); - await tester.tapImportButton(); - - final testFileNames = ['test1.md', 'test2.md']; - final paths = []; - for (final fileName in testFileNames) { - final str = await rootBundle.loadString( - 'assets/test/workspaces/markdowns/$fileName', - ); - final path = p.join(context.applicationDataDirectory, fileName); - paths.add(path); - File(path).writeAsStringSync(str); - } - // mock get files - mockPickFilePaths( - paths: testFileNames - .map((e) => p.join(context.applicationDataDirectory, e)) - .toList(), - ); - - await tester.tapTextAndMarkdownButton(); - - tester.expectToSeePageName('test1'); - tester.expectToSeePageName('test2'); - }); - - testWidgets('import markdown file with table', (tester) async { - final context = await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // expect to see a getting started page - tester.expectToSeePageName(gettingStarted); - - await tester.tapAddViewButton(); - await tester.tapImportButton(); - - const testFileName = 'markdown_with_table.md'; - final paths = []; - final str = await rootBundle.loadString( - 'assets/test/workspaces/markdowns/$testFileName', - ); - final path = p.join(context.applicationDataDirectory, testFileName); - paths.add(path); - File(path).writeAsStringSync(str); - // mock get files - mockPickFilePaths( - paths: paths, - ); - - await tester.tapTextAndMarkdownButton(); - - tester.expectToSeePageName('markdown_with_table'); - - // expect to see all content of markdown file along with table - await tester.openPage('markdown_with_table'); - - final importedPageEditorState = tester.editor.getCurrentEditorState(); - expect( - importedPageEditorState.getNodeAtPath([0])!.type, - HeadingBlockKeys.type, - ); - expect( - importedPageEditorState.getNodeAtPath([1])!.type, - HeadingBlockKeys.type, - ); - expect( - importedPageEditorState.getNodeAtPath([2])!.type, - SimpleTableBlockKeys.type, - ); - }); - }); -} 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 deleted file mode 100644 index 8c3c29ab77..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart +++ /dev/null @@ -1,159 +0,0 @@ -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: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(); - - group('share markdown in document page', () { - testWidgets('click the share button in document page', (tester) async { - final context = await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // 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()); - for (final entry in archive) { - if (entry.isFile && entry.name.endsWith('.md')) { - final markdown = utf8.decode(entry.content); - expect(markdown, expectedMarkdown); - } - } - }); - - testWidgets( - 'share the markdown after renaming the document name', - (tester) async { - final context = await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // expect to see a getting started page - tester.expectToSeePageName(gettingStarted); - - // rename the document - await tester.hoverOnPageName( - gettingStarted, - onHover: () async { - await tester.renamePage('example'); - }, - ); - - final shareButton = find.byType(ShareButton); - final shareButtonState = tester.widget(shareButton) as ShareButton; - - final path = await mockSaveFilePath( - p.join( - context.applicationDataDirectory, - '${shareButtonState.view.name}.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()); - for (final entry in archive) { - if (entry.isFile && entry.name.endsWith('.md')) { - final markdown = utf8.decode(entry.content); - expect(markdown, expectedMarkdown); - } - } - }, - ); - - 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); - }); - }); -} - -const expectedMarkdown = ''' -# Welcome to AppFlowy! -## Here are the basics -- [ ] Click anywhere and just start typing. -- [ ] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~ -- [ ] As soon as you type `/` a menu will pop up. Select different types of content blocks you can add. -- [ ] Type `/` followed by `/bullet` or `/num` to create a list. -- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page. -- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. - ---- - -## Keyboard shortcuts, markdown, and code block -1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts) -1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown) -1. Type `/code` to insert a code block -```rust -// This is the main function. -fn main() { - // Print text to the console. - println!("Hello World!"); -} -``` - -## Have a question❓ -> Click `?` at the bottom right for help and support. - -> 🥰 -> -> Like AppFlowy? Follow us: -> [GitHub](https://github.com/AppFlowy-IO/AppFlowy) -> [Twitter](https://twitter.com/appflowy): @appflowy -> [Newsletter](https://blog-appflowy.ghost.io/) -> - - - - -'''; diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart deleted file mode 100644 index b9e1303279..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/prelude.dart'; -import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('customize the folder path', () { - if (Platform.isWindows) { - return; - } - - // testWidgets('switch to B from A, then switch to A again', (tester) async { - // const userA = 'UserA'; - // const userB = 'UserB'; - - // final initialPath = p.join(userA, appFlowyDataFolder); - // final context = await tester.initializeAppFlowy( - // pathExtension: initialPath, - // ); - // // remove the last extension - // final rootPath = context.applicationDataDirectory.replaceFirst( - // initialPath, - // '', - // ); - - // await tester.tapGoButton(); - // await tester.expectToSeeHomePageWithGetStartedPage(); - - // // switch to user B - // { - // // set user name for userA - // await tester.openSettings(); - // await tester.openSettingsPage(SettingsPage.user); - // await tester.enterUserName(userA); - - // await tester.openSettingsPage(SettingsPage.files); - // await tester.pumpAndSettle(); - - // // mock the file_picker result - // await mockGetDirectoryPath( - // p.join(rootPath, userB), - // ); - // await tester.tapCustomLocationButton(); - // await tester.pumpAndSettle(); - // await tester.expectToSeeHomePageWithGetStartedPage(); - - // // set user name for userB - // await tester.openSettings(); - // await tester.openSettingsPage(SettingsPage.user); - // await tester.enterUserName(userB); - // } - - // // switch to the userA - // { - // await tester.openSettingsPage(SettingsPage.files); - // await tester.pumpAndSettle(); - - // // mock the file_picker result - // await mockGetDirectoryPath( - // p.join(rootPath, userA), - // ); - // await tester.tapCustomLocationButton(); - - // await tester.pumpAndSettle(); - // await tester.expectToSeeHomePageWithGetStartedPage(); - // tester.expectToSeeUserName(userA); - // } - - // // switch to the userB again - // { - // await tester.openSettings(); - // await tester.openSettingsPage(SettingsPage.files); - // await tester.pumpAndSettle(); - - // // mock the file_picker result - // await mockGetDirectoryPath( - // p.join(rootPath, userB), - // ); - // await tester.tapCustomLocationButton(); - - // await tester.pumpAndSettle(); - // await tester.expectToSeeHomePageWithGetStartedPage(); - // tester.expectToSeeUserName(userB); - // } - // }); - - testWidgets('reset to default location', (tester) async { - await tester.initializeAppFlowy(); - - await tester.tapAnonymousSignInButton(); - - // home and readme document - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open settings and restore the location - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.manageData); - await tester.restoreLocation(); - - expect( - await appFlowyApplicationDataDirectory().then((value) => value.path), - await getIt().getPath(), - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart deleted file mode 100644 index 63ec958c54..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart +++ /dev/null @@ -1,371 +0,0 @@ -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/keyboard.dart'; -import '../../shared/util.dart'; - -const _documentName = 'First Doc'; -const _documentTwoName = 'Second Doc'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Tabs', () { - testWidgets('open/navigate/close tabs', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // No tabs rendered yet - expect(find.byType(FlowyTab), 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), - ); - - /// Navigate to the second tab - await tester.tap( - find.descendant( - of: find.byType(FlowyTab), - matching: find.text(gettingStarted), - ), - ); - - /// Close tab by shortcut - await FlowyTestKeyboard.simulateKeyDownEvent( - [ - Platform.isMacOS - ? LogicalKeyboardKey.meta - : LogicalKeyboardKey.control, - LogicalKeyboardKey.keyW, - ], - tester: tester, - ); - - expect( - find.descendant( - of: find.byType(TabsManager), - 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 deleted file mode 100644 index c91ba21edb..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_1.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'desktop/document/document_test_runner_1.dart' as document_test_runner_1; -import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test; - -Future main() async { - await runIntegration1OnDesktop(); -} - -Future runIntegration1OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - switch_folder_test.main(); - document_test_runner_1.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_2.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_2.dart deleted file mode 100644 index 99d6f7d58f..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_2.dart +++ /dev/null @@ -1,17 +0,0 @@ -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; - -Future main() async { - await runIntegration2OnDesktop(); -} - -Future runIntegration2OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - - database_test_runner_1.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 deleted file mode 100644 index a9d3783f1d..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart +++ /dev/null @@ -1,19 +0,0 @@ -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; - -Future main() async { - await runIntegration3OnDesktop(); -} - -Future runIntegration3OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - - board_test_runner.main(); - grid_test_runner_1.main(); - // DON'T add more tests here. -} 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/document/cover_image_test.dart b/frontend/appflowy_flutter/integration_test/document/cover_image_test.dart new file mode 100644 index 0000000000..0d3708d1f2 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/cover_image_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('cover image', () { + const location = 'cover_image'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets( + 'hovering on cover image will display change and delete cover image buttons', + (tester) async { + await tester.initializeAppFlowy(); + + await tester.tapGoButton(); + await tester.editor.hoverOnCoverPluginAddButton(); + + tester.expectToSeePluginAddCoverAndIconButton(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/document/document_test.dart b/frontend/appflowy_flutter/integration_test/document/document_test.dart new file mode 100644 index 0000000000..3efbbee43b --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/document_test.dart @@ -0,0 +1,90 @@ +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'; + +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('document', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('create a new document when launching app in first time', + (tester) async { + await tester.initializeAppFlowy(); + + await tester.tapGoButton(); + + // create a new document + await tester.tapAddButton(); + await tester.tapCreateDocumentButton(); + await tester.pumpAndSettle(); + + // expect to see a new document + tester.expectToSeePageName( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + ); + // and with one paragraph block + expect(find.byType(TextBlockComponentWidget), findsOneWidget); + }); + + testWidgets('delete the readme page and restore it', (tester) async { + await tester.initializeAppFlowy(); + + await tester.tapGoButton(); + + // delete the readme page + await tester.hoverOnPageName(readme); + await tester.tapDeletePageButton(); + + // the banner should show up and the readme page should be gone + tester.expectToSeeDocumentBanner(); + tester.expectNotToSeePageName(readme); + + // restore the readme page + await tester.tapRestoreButton(); + + // the banner should be gone and the readme page should be back + tester.expectNotToSeeDocumentBanner(); + tester.expectToSeePageName(readme); + }); + + testWidgets('delete the readme page and delete it permanently', + (tester) async { + await tester.initializeAppFlowy(); + + await tester.tapGoButton(); + + // delete the readme page + await tester.hoverOnPageName(readme); + await tester.tapDeletePageButton(); + + // the banner should show up and the readme page should be gone + tester.expectToSeeDocumentBanner(); + tester.expectNotToSeePageName(readme); + + // delete the page permanently + await tester.tapDeletePermanentlyButton(); + + // the banner should be gone and the readme page should be gone + tester.expectNotToSeeDocumentBanner(); + tester.expectNotToSeePageName(readme); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart new file mode 100644 index 0000000000..5f6cea1d18 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart @@ -0,0 +1,110 @@ +import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('database view in document', () { + const location = 'database_view'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('insert a referenced grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await insertReferenceDatabase(tester, ViewLayoutPB.Grid); + + // validate the referenced grid is inserted + expect( + find.descendant( + of: find.byType(AppFlowyEditor), + matching: find.byType(GridPage), + ), + findsOneWidget, + ); + }); + + testWidgets('insert a referenced board', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await insertReferenceDatabase(tester, ViewLayoutPB.Board); + + // validate the referenced board is inserted + expect( + find.descendant( + of: find.byType(AppFlowyEditor), + matching: find.byType(BoardPage), + ), + findsOneWidget, + ); + }); + + // testWidgets('insert a referenced calendar', (tester) async { + // await tester.initializeAppFlowy(); + // await tester.tapGoButton(); + + // await insertReferenceDatabase(tester, ViewLayoutPB.Calendar); + + // // validate the referenced grid is inserted + // expect( + // find.descendant( + // of: find.byType(AppFlowyEditor), + // matching: find.byType(CalendarPage), + // ), + // findsOneWidget, + // ); + // }); + }); +} + +/// Insert a referenced database of [layout] into the document +Future insertReferenceDatabase( + WidgetTester tester, + ViewLayoutPB layout, +) async { + // create a new grid + final id = uuid(); + final name = '${layout.name}_$id'; + await tester.createNewPageWithName( + layout, + name, + ); + // create a new document + await tester.createNewPageWithName( + ViewLayoutPB.Document, + 'insert_a_reference_${layout.name}', + ); + // tap the first line of the document + await tester.editor.tapLineOfEditorAt(0); + // insert a referenced grid + await tester.editor.showSlashMenu(); + await tester.editor.tapSlashMenuItemWithName( + layout.referencedMenuName, + ); + + final linkToPageMenu = find.byType(LinkToPageMenu); + expect(linkToPageMenu, findsOneWidget); + final referencedDatabase = find.descendant( + of: linkToPageMenu, + matching: find.findTextInFlowyText(name), + ); + expect(referencedDatabase, findsOneWidget); + await tester.tapButton(referencedDatabase); +} diff --git a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart new file mode 100644 index 0000000000..10dd472362 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart @@ -0,0 +1,141 @@ +import 'dart:io'; + +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +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 '../util/ime.dart'; +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('edit document', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('redo & undo', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // create a new document called Sample + const pageName = 'Sample'; + await tester.createNewPageWithName(ViewLayoutPB.Document, pageName); + + // focus on the editor + await tester.editor.tapLineOfEditorAt(0); + + // insert 1. to trigger it to be a numbered list + await tester.ime.insertText('1. '); + expect(find.text('1.', findRichText: true), findsOneWidget); + expect( + tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, + NumberedListBlockKeys.type, + ); + + // undo + // numbered list will be reverted to paragraph + await tester.simulateKeyEvent( + LogicalKeyboardKey.keyZ, + isControlPressed: Platform.isWindows || Platform.isLinux, + isMetaPressed: Platform.isMacOS, + ); + expect( + tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, + ParagraphBlockKeys.type, + ); + + // redo + await tester.simulateKeyEvent( + LogicalKeyboardKey.keyZ, + isControlPressed: Platform.isWindows || Platform.isLinux, + isMetaPressed: Platform.isMacOS, + isShiftPressed: true, + ); + expect( + tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, + NumberedListBlockKeys.type, + ); + + // switch to other page and switch back + await tester.openPage(readme); + await tester.openPage(pageName); + + // the numbered list should be kept + expect( + tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type, + NumberedListBlockKeys.type, + ); + }); + + testWidgets('write a readme document', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // create a new document called Sample + const pageName = 'Sample'; + await tester.createNewPageWithName(ViewLayoutPB.Document, pageName); + + // focus on the editor + await tester.editor.tapLineOfEditorAt(0); + + // mock inputting the sample + final lines = _sample.split('\n'); + for (final line in lines) { + await tester.ime.insertText(line); + await tester.ime.insertCharacter('\n'); + } + + // switch to other page and switch back + await tester.openPage(readme); + await tester.openPage(pageName); + + // this screenshots are different on different platform, so comment it out temporarily. + // check the document + // await expectLater( + // find.byType(AppFlowyEditor), + // matchesGoldenFile('document/edit_document_test.png'), + // ); + }); + }); +} + +// TODO(Lucas.Xu): there're no shorctcuts for underline, format code yet. +const _sample = r''' +# Heading 1 +## Heading 2 +### Heading 3 +--- +[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~ + +[] Type / followed by /bullet or /num to create a list. + +[x] Click `+ New Page` button at the bottom of your sidebar to add a new page. + +[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. +--- +* bulleted list 1 + +* bulleted list 2 + +* bulleted list 3 +bulleted list 4 +--- +1. numbered list 1 + +2. numbered list 2 + +3. numbered list 3 +numbered list 4 +--- +" quote'''; diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.png b/frontend/appflowy_flutter/integration_test/document/edit_document_test.png similarity index 100% rename from frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.png rename to frontend/appflowy_flutter/integration_test/document/edit_document_test.png diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_document_test.dart b/frontend/appflowy_flutter/integration_test/empty_document_test.dart similarity index 97% rename from frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_document_test.dart rename to frontend/appflowy_flutter/integration_test/empty_document_test.dart index 6712e6959d..1483a576ac 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_document_test.dart +++ b/frontend/appflowy_flutter/integration_test/empty_document_test.dart @@ -3,9 +3,8 @@ 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 '../../shared/keyboard.dart'; -import '../../shared/util.dart'; +import 'util/keyboard.dart'; +import 'util/util.dart'; /// Integration tests for an empty document. The [TestWorkspaceService] will load a workspace from an empty document `assets/test/workspaces/empty_document.zip` for all tests. /// @@ -29,8 +28,8 @@ void main() { const service = TestWorkspaceService(TestWorkspace.emptyDocument); group('Tests on a workspace with only an empty document', () { - setUpAll(() async => service.setUpAll()); - setUp(() async => service.setUp()); + setUpAll(() async => await service.setUpAll()); + setUp(() async => await service.setUp()); testWidgets('/board shortcut creates a new board and view of the board', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/import_files_test.dart b/frontend/appflowy_flutter/integration_test/import_files_test.dart new file mode 100644 index 0000000000..5743e01f76 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/import_files_test.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/mock/mock_file_picker.dart'; +import 'util/util.dart'; +import 'package:path/path.dart' as p; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('import files', () { + const location = 'import_files'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('import multiple markdown files', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // expect to see a readme page + tester.expectToSeePageName(readme); + + await tester.tapAddButton(); + await tester.tapImportButton(); + + final testFileNames = ['test1.md', 'test2.md']; + final fileLocation = await tester.currentFileLocation(); + for (final fileName in testFileNames) { + final str = await rootBundle.loadString( + p.join( + 'assets/test/workspaces/markdowns', + fileName, + ), + ); + File(p.join(fileLocation, fileName)).writeAsStringSync(str); + } + // mock get files + await mockPickFilePaths(testFileNames, name: location); + + await tester.tapTextAndMarkdownButton(); + + tester.expectToSeePageName('test1'); + tester.expectToSeePageName('test2'); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/language_test.dart b/frontend/appflowy_flutter/integration_test/language_test.dart similarity index 77% rename from frontend/appflowy_flutter/integration_test/desktop/uncategorized/language_test.dart rename to frontend/appflowy_flutter/integration_test/language_test.dart index 3c07a2df5e..40b774a1d9 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/language_test.dart +++ b/frontend/appflowy_flutter/integration_test/language_test.dart @@ -1,13 +1,23 @@ -import 'package:appflowy/user/presentation/screens/skip_log_in_screen.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../../shared/util.dart'; +import 'util/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('document', () { + const location = 'appflowy'; + + setUpAll(() async { + await TestFolder.setTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + testWidgets( 'change the language successfully when launching the app for the first time', (tester) async { 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 deleted file mode 100644 index d64ab094de..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/presentation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('create new page in home page:', () { - testWidgets('create document', (tester) async { - await tester.launchInAnonymousMode(); - - // tap the create page button - final createPageButton = find.byWidgetPredicate( - (widget) => - widget is FlowySvg && - widget.svg.path == FlowySvgs.m_home_add_m.path, - ); - await tester.tapButton(createPageButton); - await tester.pumpAndSettle(); - expect(find.byType(MobileDocumentScreen), 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 deleted file mode 100644 index ab98ca190a..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy/mobile/presentation/home/home.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('anonymous sign in on mobile:', () { - testWidgets('anon user and then sign in', (tester) async { - await tester.launchInAnonymousMode(); - - // expect to see the home page - expect(find.byType(MobileHomeScreen), findsOneWidget); - }); - }); -} 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/open_ai_smart_menu_test.dart b/frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart new file mode 100644 index 0000000000..75c9834ee8 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/open_ai_smart_menu_test.dart @@ -0,0 +1,108 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +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 'util/mock/mock_openai_repository.dart'; +import 'util/util.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; +import 'package:appflowy/startup/startup.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + const service = TestWorkspaceService(TestWorkspace.aiWorkSpace); + + group('integration tests for open-ai smart menu', () { + setUpAll(() async => await service.setUpAll()); + setUp(() async => await 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], offset: 0), + 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], offset: 0), + end: Position(path: [3], offset: 0), + ), + ); + }); + }); +} + +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/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index 0fc3c5d826..8dc301ddbd 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -1,15 +1,23 @@ -import 'dart:io'; +import 'package:integration_test/integration_test.dart'; -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 'switch_folder_test.dart' as switch_folder_test; +import 'document/document_test.dart' as document_test; +import 'document/cover_image_test.dart' as cover_image_test; +import 'share_markdown_test.dart' as share_markdown_test; +import 'import_files_test.dart' as import_files_test; +import 'document/document_with_database_test.dart' + as document_with_database_test; +import 'document/edit_document_test.dart' as edit_document_test; +import 'database_cell_test.dart' as database_cell_test; +import 'database_field_test.dart' as database_field_test; +import 'database_share_test.dart' as database_share_test; +import 'database_row_page_test.dart' as database_row_page_test; +import 'database_row_test.dart' as database_row_test; +import 'database_setting_test.dart' as database_setting_test; +import 'database_filter_test.dart' as database_filter_test; +import 'database_view_test.dart' as database_view_test; +import 'database_calendar_test.dart' as database_calendar_test; +import 'database_sort_test.dart' as database_sort_test; /// The main task runner for all integration tests in AppFlowy. /// @@ -18,20 +26,31 @@ import 'mobile_runner_1.dart'; /// If flutter/flutter#101031 is resolved, this file can be removed completely. /// Once removed, the integration_test.yaml must be updated to exclude this as /// as the test target. -Future main() async { - if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { - 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(); - } else { - throw Exception('Unsupported platform'); - } +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + switch_folder_test.main(); + share_markdown_test.main(); + import_files_test.main(); + + // Document integration tests + cover_image_test.main(); + document_test.main(); + document_with_database_test.main(); + edit_document_test.main(); + + // Database integration tests + database_cell_test.main(); + database_field_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(); + + // board_test.main(); + // empty_document_test.main(); + // smart_menu_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/share_markdown_test.dart new file mode 100644 index 0000000000..a39575e49b --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/share_markdown_test.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +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 'util/mock/mock_file_picker.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('share markdown in document page', () { + const location = 'markdown'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('click the share button in document page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // expect to see a readme page + tester.expectToSeePageName(readme); + + // mock the file picker + final path = await mockSaveFilePath(location, 'test.md'); + // 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); + final isExist = file.existsSync(); + expect(isExist, true); + final markdown = file.readAsStringSync(); + expect(markdown, expectedMarkdown); + }); + + testWidgets( + 'share the markdown after renaming the document name', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // expect to see a readme page + tester.expectToSeePageName(readme); + + // rename the document + await tester.hoverOnPageName(readme); + await tester.renamePage('example'); + + final shareButton = find.byType(ShareActionList); + final shareButtonState = + tester.state(shareButton) as ShareActionListState; + final path = + await mockSaveFilePath(location, '${shareButtonState.name}.md'); + + // 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); + final isExist = file.existsSync(); + expect(isExist, true); + }, + ); + }); +} + +const expectedMarkdown = r''' +# Welcome to AppFlowy! +## Here are the basics +- [ ] Click anywhere and just start typing. +- [ ] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~ +- [ ] As soon as you type `/` a menu will pop up. Select different types of content blocks you can add. +- [ ] Type `/` followed by `/bullet` or `/num` to create a list. +- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page. +- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. + +--- + +## Keyboard shortcuts, markdown, and code block +1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts) +1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown) +1. Type `/code` to insert a code block + +## Have a question❓ +> Click `?` at the bottom right for help and support. + + + +'''; diff --git a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart deleted file mode 100644 index 88f9634afd..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart +++ /dev/null @@ -1,73 +0,0 @@ -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/widgets/setting_appflowy_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, - ); - } - - /// Requires being on the SettingsPage.account of the SettingsDialog - Future logout() async { - final scrollable = find.findSettingsScrollable(); - await scrollUntilVisible( - find.byType(AccountSignInOutButton), - 100, - scrollable: scrollable, - ); - - await tapButton(find.byType(AccountSignInOutButton)); - - expectToSeeText(LocaleKeys.button_ok.tr()); - await tapButtonWithName(LocaleKeys.button_ok.tr()); - } - - Future tapSignInAsGuest() async { - await tapButton(find.byType(SignInAnonymousButtonV2)); - } - - void expectToSeeGoogleLoginButton() { - expect(find.byKey(signInWithGoogleButtonKey), findsOneWidget); - } - - void assertSwitchValue(Finder finder, bool value) { - final Switch switchWidget = widget(finder); - final isSwitched = switchWidget.value; - assert(isSwitched == value); - } - - void assertToggleValue(Finder finder, bool value) { - final Toggle switchWidget = widget(finder); - final isSwitched = switchWidget.value; - assert(isSwitched == value); - } - - void assertAppFlowyCloudEnableSyncSwitchValue(bool value) { - assertToggleValue( - find.descendant( - of: find.byType(AppFlowyCloudEnableSync), - matching: find.byWidgetPredicate((widget) => widget is Toggle), - ), - value, - ); - } - - Future toggleEnableSync(Type syncButton) async { - final finder = find.descendant( - of: find.byType(syncButton), - matching: find.byWidgetPredicate((widget) => widget is Toggle), - ); - - await tapButton(finder); - } -} diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart deleted file mode 100644 index 493cb4c1f0..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -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/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}); - - final String applicationDataDirectory; -} - -extension AppFlowyTestBase on WidgetTester { - Future initializeAppFlowy({ - // use to append after the application data directory - String? pathExtension, - // use to specify the application data directory, if not specified, a temporary directory will be used. - String? dataDirectory, - Size windowSize = const Size(1600, 1200), - AuthenticatorType? cloudType, - String? email, - }) async { - if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - // Set the window size - await binding.setSurfaceSize(windowSize); - } - - mockHotKeyManagerHandlers(); - final applicationDataDirectory = dataDirectory ?? - await mockApplicationDataStorage( - pathExtension: pathExtension, - ); - - await FlowyRunner.run( - AppFlowyApplication(), - IntegrationMode.integrationTest, - rustEnvsBuilder: () { - final rustEnvs = {}; - if (cloudType != null) { - switch (cloudType) { - case AuthenticatorType.local: - break; - case AuthenticatorType.appflowyCloudSelfHost: - rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; - rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; - break; - default: - throw Exception("not supported"); - } - } - return rustEnvs; - }, - didInitGetItCallback: () { - return Future( - () async { - if (cloudType != null) { - switch (cloudType) { - case AuthenticatorType.local: - await useLocalServer(); - break; - case AuthenticatorType.appflowyCloudSelfHost: - await useTestSelfHostedAppFlowyCloud(); - getIt.unregister(); - getIt.registerFactory( - () => AppFlowyCloudMockAuthService(email: email), - ); - default: - throw Exception("not supported"); - } - } - }, - ); - }, - ); - - await waitUntilSignInPageShow(); - return FlowyTestContext( - applicationDataDirectory: applicationDataDirectory, - ); - } - - void mockHotKeyManagerHandlers() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(const MethodChannel('hotkey_manager'), - (MethodCall methodCall) async { - if (methodCall.method == 'unregisterAll') { - // do nothing - } - return; - }); - } - - Future waitUntilSignInPageShow() async { - if (isAuthEnabled || UniversalPlatform.isMobile) { - final finder = find.byType(SignInAnonymousButtonV2); - await pumpUntilFound(finder, timeout: const Duration(seconds: 30)); - expect(finder, findsOneWidget); - } else { - final finder = find.byType(GoButton); - await pumpUntilFound(finder); - expect(finder, findsOneWidget); - } - } - - Future waitForSeconds(int seconds) async { - await Future.delayed(Duration(seconds: seconds), () {}); - } - - Future pumpUntilFound( - Finder finder, { - Duration timeout = const Duration(seconds: 10), - Duration pumpInterval = const Duration( - milliseconds: 50, - ), // Interval between pumps - }) async { - bool timerDone = false; - final timer = Timer(timeout, () => timerDone = true); - while (!timerDone) { - await pump(pumpInterval); // Pump with an interval - if (any(finder)) { - break; - } - } - timer.cancel(); - } - - Future pumpUntilNotFound( - Finder finder, { - Duration timeout = const Duration(seconds: 10), - Duration pumpInterval = const Duration( - milliseconds: 50, - ), // Interval between pumps - }) async { - bool timerDone = false; - final timer = Timer(timeout, () => timerDone = true); - while (!timerDone) { - await pump(pumpInterval); // Pump with an interval - if (!any(finder)) { - break; - } - } - timer.cancel(); - } - - Future tapButton( - Finder finder, { - 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, - buttons: buttons, - kind: kind, - ); - await gesture.cancel(); - await gesture.down(location); - await gesture.cancel(); - if (pumpAndSettle) { - await this.pumpAndSettle( - Duration(milliseconds: milliseconds), - EnginePhase.sendSemanticsUpdate, - const Duration(seconds: 15), - ); - } - } - - Future tapButtonWithName( - String tr, { - int milliseconds = 500, - bool pumpAndSettle = true, - }) async { - Finder button = find.text(tr, findRichText: true, skipOffstage: false); - if (button.evaluate().isEmpty) { - button = find.byWidgetPredicate( - (widget) => widget is FlowyText && widget.text == tr, - ); - } - await tapButton( - button, - milliseconds: milliseconds, - pumpAndSettle: pumpAndSettle, - ); - } - - Future doubleTapAt( - Offset location, { - int? pointer, - int buttons = kPrimaryButton, - int milliseconds = 500, - }) async { - await tapAt(location, pointer: pointer, buttons: buttons); - await pump(kDoubleTapMinTime); - await tapAt(location, pointer: pointer, buttons: buttons); - await pumpAndSettle(Duration(milliseconds: milliseconds)); - } - - 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 { - Finder findTextInFlowyText(String text) { - return find.byWidgetPredicate( - (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 useTestSelfHostedAppFlowyCloud() async { - await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl); -} - -Future mockApplicationDataStorage({ - // use to append after the application data directory - String? pathExtension, -}) async { - final dir = await getTemporaryDirectory(); - - // Use a random uuid to avoid conflict. - String path = p.join(dir.path, 'appflowy_integration_test', uuid()); - if (pathExtension != null && pathExtension.isNotEmpty) { - path = '$path/$pathExtension'; - } - final directory = Directory(path); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - - MockApplicationDataStorage.initialPath = directory.path; - - return directory.path; -} diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart deleted file mode 100644 index d7a505d152..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ /dev/null @@ -1,1101 +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/mobile/presentation/base/type_option_menu_item.dart'; -import 'package:appflowy/mobile/presentation/presentation.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart'; -import 'package:appflowy/plugins/shared/share/share_button.dart'; -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/presentation/screens/screens.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; -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'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.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/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/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'; - -extension CommonOperations on WidgetTester { - /// Tap the GetStart button on the launch page. - Future tapAnonymousSignInButton() async { - // local version - final goButton = find.byType(GoButton); - if (goButton.evaluate().isNotEmpty) { - await tapButton(goButton); - } else { - // cloud version - final anonymousButton = find.byType(SignInAnonymousButtonV2); - await tapButton(anonymousButton, warnIfMissed: true); - } - - 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)); - } - } - - /// Tap the + button on the home page. - Future tapAddViewButton({ - String name = gettingStarted, - ViewLayoutPB layout = ViewLayoutPB.Document, - }) async { - await hoverOnPageName( - name, - onHover: () async { - final addButton = find.byType(ViewAddButton); - await tapButton(addButton); - }, - ); - } - - /// Tap the 'New Page' Button on the sidebar. - Future tapNewPageButton() async { - final newPageButton = find.byType(SidebarNewPageButton); - await tapButton(newPageButton); - } - - /// Tap the import button. - /// - /// Must call [tapAddViewButton] first. - Future tapImportButton() async { - await tapButtonWithName(LocaleKeys.moreAction_import.tr()); - } - - /// Tap the import from text & markdown button. - /// - /// Must call [tapImportButton] first. - Future tapTextAndMarkdownButton() async { - await tapButtonWithName(LocaleKeys.importPanel_textAndMarkdown.tr()); - } - - /// Tap the LanguageSelectorOnWelcomePage widget on the launch page. - Future tapLanguageSelectorOnWelcomePage() async { - final languageSelector = find.byType(LanguageSelectorOnWelcomePage); - await tapButton(languageSelector); - } - - /// Tap languageItem on LanguageItemsListView. - /// - /// [scrollDelta] is the distance to scroll the ListView. - /// Default value is 100 - /// - /// If it is positive -> scroll down. - /// - /// If it is negative -> scroll up. - Future tapLanguageItem({ - required String languageCode, - String? countryCode, - double? scrollDelta, - }) async { - final languageItemsListView = find.descendant( - of: find.byType(ListView), - matching: find.byType(Scrollable), - ); - - final languageItem = find.byWidgetPredicate( - (widget) => - widget is LanguageItem && - widget.locale.languageCode == languageCode && - widget.locale.countryCode == countryCode, - ); - - // scroll the ListView until zHCNLanguageItem shows on the screen. - await scrollUntilVisible( - languageItem, - scrollDelta ?? 100, - scrollable: languageItemsListView, - // maxHeight of LanguageItemsListView - maxScrolls: 400, - ); - - try { - await tapButton(languageItem); - } on FlutterError catch (e) { - Log.warn('tapLanguageItem error: $e'); - } - } - - /// Hover on the widget. - Future hoverOnWidget( - Finder finder, { - Offset? offset, - Future Function()? onHover, - bool removePointer = true, - }) async { - try { - final gesture = await createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: offset ?? getCenter(finder)); - await pumpAndSettle(); - await onHover?.call(); - await gesture.removePointer(); - } catch (err) { - Log.error('hoverOnWidget error: $err'); - } - } - - /// Hover on the page name. - Future hoverOnPageName( - String name, { - ViewLayoutPB layout = ViewLayoutPB.Document, - Future Function()? onHover, - bool useLast = true, - }) async { - final pageNames = findPageName(name, layout: layout); - if (useLast) { - await hoverOnWidget(pageNames.last, onHover: onHover); - } else { - await hoverOnWidget(pageNames.first, onHover: onHover); - } - } - - /// 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, { - ViewLayoutPB layout = ViewLayoutPB.Document, - }) async { - final finder = findPageName(name, layout: layout); - expect(finder, findsOneWidget); - await tapButton(finder); - } - - /// Tap the ... button beside the page name. - /// - /// Must call [hoverOnPageName] first. - Future tapPageOptionButton() async { - final optionButton = find.descendant( - of: find.byType(ViewMoreActionPopover), - matching: find.byFlowySvg(FlowySvgs.workspace_three_dots_s), - ); - await tapButton(optionButton); - } - - /// Tap the delete page button. - Future tapDeletePageButton() async { - await tapPageOptionButton(); - await tapButtonWithName(ViewMoreActionType.delete.name); - } - - /// Tap the rename page button. - Future tapRenamePageButton() async { - await tapPageOptionButton(); - await tapButtonWithName(ViewMoreActionType.rename.name); - } - - /// Tap the favorite page button - Future tapFavoritePageButton() async { - await tapPageOptionButton(); - await tapButtonWithName(ViewMoreActionType.favorite.name); - } - - /// Tap the unfavorite page button - Future tapUnfavoritePageButton() async { - await tapPageOptionButton(); - await tapButtonWithName(ViewMoreActionType.unFavorite.name); - } - - /// Tap the Open in a new tab button - Future tapOpenInTabButton() async { - await tapPageOptionButton(); - await tapButtonWithName(ViewMoreActionType.openInNewTab.name); - } - - /// Rename the page. - Future renamePage(String name) async { - await tapRenamePageButton(); - await enterText(find.byType(TextFormField), name); - await tapOKButton(); - } - - Future tapTrashButton() async { - await tap(find.byType(SidebarTrashButton)); - } - - Future tapOKButton() async { - final okButton = find.byWidgetPredicate( - (widget) => - widget is PrimaryTextButton && - widget.label == LocaleKeys.button_ok.tr(), - ); - 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. - Future tapRestoreButton() async { - final restoreButton = find.textContaining( - LocaleKeys.deletePagePrompt_restore.tr(), - ); - await tapButton(restoreButton); - } - - /// Tap the delete permanently button. - /// - /// the delete permanently button will show after the current page is deleted. - Future tapDeletePermanentlyButton() async { - final deleteButton = find.textContaining( - LocaleKeys.deletePagePrompt_deletePermanent.tr(), - ); - await tapButton(deleteButton); - await tap(find.text(LocaleKeys.button_delete.tr())); - await pumpAndSettle(); - } - - /// Tap the share button above the document page. - Future tapShareButton() async { - final shareButton = find.byWidgetPredicate( - (widget) => widget is ShareButton, - ); - 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. - Future tapMarkdownButton() async { - final markdownButton = find.textContaining( - LocaleKeys.shareAction_markdown.tr(), - ); - await tapButton(markdownButton); - } - - Future createNewPageWithNameUnderParent({ - String? name, - ViewLayoutPB layout = ViewLayoutPB.Document, - String? parentName, - bool openAfterCreated = true, - }) async { - // create a new page - await tapAddViewButton(name: parentName ?? gettingStarted, layout: layout); - await tapButtonWithName(layout.menuName); - final settingsOrFailure = await getIt().getWithFormat( - KVKeys.showRenameDialogWhenCreatingNewFile, - (value) => bool.parse(value), - ); - final showRenameDialog = settingsOrFailure ?? false; - if (showRenameDialog) { - await tapOKButton(); - } - await pumpAndSettle(); - - // hover on it and change it's name - if (name != null) { - await hoverOnPageName( - layout.defaultName, - layout: layout, - onHover: () async { - await renamePage(name); - await pumpAndSettle(); - }, - ); - await pumpAndSettle(); - } - - // open the page after created - if (openAfterCreated) { - await openPage( - // if the name is null, use the default name - name ?? layout.defaultName, - 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, - }) async { - await tapButton(find.byType(SidebarNewPageButton)); - } - - Future simulateKeyEvent( - LogicalKeyboardKey key, { - bool isControlPressed = false, - bool isShiftPressed = false, - bool isAltPressed = false, - bool isMetaPressed = false, - PhysicalKeyboardKey? physicalKey, - }) async { - if (isControlPressed) { - await simulateKeyDownEvent(LogicalKeyboardKey.control); - } - if (isShiftPressed) { - await simulateKeyDownEvent(LogicalKeyboardKey.shift); - } - if (isAltPressed) { - await simulateKeyDownEvent(LogicalKeyboardKey.alt); - } - if (isMetaPressed) { - await simulateKeyDownEvent(LogicalKeyboardKey.meta); - } - await simulateKeyDownEvent( - key, - physicalKey: physicalKey, - ); - await simulateKeyUpEvent( - key, - physicalKey: physicalKey, - ); - if (isControlPressed) { - await simulateKeyUpEvent(LogicalKeyboardKey.control); - } - if (isShiftPressed) { - await simulateKeyUpEvent(LogicalKeyboardKey.shift); - } - if (isAltPressed) { - await simulateKeyUpEvent(LogicalKeyboardKey.alt); - } - if (isMetaPressed) { - await simulateKeyUpEvent(LogicalKeyboardKey.meta); - } - await pumpAndSettle(); - } - - Future openAppInNewTab(String name, ViewLayoutPB layout) async { - await hoverOnPageName( - name, - onHover: () async { - await tapOpenInTabButton(); - await pumpAndSettle(); - }, - ); - await pumpAndSettle(); - } - - Future favoriteViewByName( - String name, { - ViewLayoutPB layout = ViewLayoutPB.Document, - }) async { - await hoverOnPageName( - name, - layout: layout, - onHover: () async { - await tapFavoritePageButton(); - await pumpAndSettle(); - }, - ); - } - - Future unfavoriteViewByName( - String name, { - ViewLayoutPB layout = ViewLayoutPB.Document, - }) async { - await hoverOnPageName( - name, - layout: layout, - onHover: () async { - await tapUnfavoritePageButton(); - await pumpAndSettle(); - }, - ); - } - - Future movePageToOtherPage({ - required String name, - required String parentName, - required ViewLayoutPB layout, - required ViewLayoutPB parentLayout, - DraggableHoverPosition position = DraggableHoverPosition.center, - }) async { - final from = findPageName(name, layout: layout); - final to = findPageName(parentName, layout: parentLayout); - final gesture = await startGesture(getCenter(from)); - Offset offset = Offset.zero; - switch (position) { - case DraggableHoverPosition.center: - offset = getCenter(to); - break; - case DraggableHoverPosition.top: - offset = getTopLeft(to); - break; - case DraggableHoverPosition.bottom: - offset = getBottomLeft(to); - break; - default: - } - await gesture.moveTo(offset, timeStamp: const Duration(milliseconds: 400)); - await gesture.up(); - 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( - (widget) => widget is FlowySvg && widget.svg.path == svg.path, - ); - await tapButton(button); - } - - // update the page icon in the sidebar - Future updatePageIconInSidebarByName({ - required String name, - String? parentName, - required ViewLayoutPB layout, - required EmojiIconData icon, - }) async { - final iconButton = find.descendant( - of: findPageName( - name, - layout: layout, - parentName: parentName, - ), - matching: - 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 pumpAndSettle(); - } - - // update the page icon in the sidebar - Future updatePageIconInTitleBarByName({ - required String name, - required ViewLayoutPB layout, - required EmojiIconData icon, - }) 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)); - 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 pumpAndSettle(); - } - - Future openNotificationHub({int tabIndex = 0}) async { - final finder = find.descendant( - of: find.byType(NotificationButton), - matching: find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.clock_alarm_s, - ), - ); - - await tap(finder); - await pumpAndSettle(); - - if (tabIndex == 1) { - final tabFinder = find.descendant( - of: find.byType(NotificationTabBar), - matching: find.byType(FlowyTabItem).at(1), - ); - - await tap(tabFinder); - await pumpAndSettle(); - } - } - - Future toggleCommandPalette() async { - // Press CMD+P or CTRL+P to open the command palette - await simulateKeyEvent( - LogicalKeyboardKey.keyP, - isControlPressed: !Platform.isMacOS, - isMetaPressed: Platform.isMacOS, - ); - await pumpAndSettle(); - } - - Future openCollaborativeWorkspaceMenu() async { - if (!FeatureFlag.collaborativeWorkspace.isOn) { - throw UnsupportedError('Collaborative workspace is not enabled'); - } - - final workspace = find.byType(SidebarWorkspace); - expect(workspace, findsOneWidget); - - await tapButton(workspace, pumpAndSettle: false); - await pump(const Duration(seconds: 5)); - } - - Future createCollaborativeWorkspace(String name) async { - if (!FeatureFlag.collaborativeWorkspace.isOn) { - throw UnsupportedError('Collaborative workspace is not enabled'); - } - await openCollaborativeWorkspaceMenu(); - // expect to see the workspace list, and there should be only one workspace - final workspacesMenu = find.byType(WorkspacesMenu); - expect(workspacesMenu, findsOneWidget); - - // click the create button - final createButton = find.byKey(createWorkspaceButtonKey); - expect(createButton, findsOneWidget); - await tapButton(createButton, pumpAndSettle: false); - await pump(const Duration(seconds: 5)); - - // see the create workspace dialog - final createWorkspaceDialog = find.byType(CreateWorkspaceDialog); - expect(createWorkspaceDialog, findsOneWidget); - - // input the workspace name - final workspaceNameInput = find.descendant( - of: createWorkspaceDialog, - matching: find.byType(TextField), - ); - await enterText(workspaceNameInput, name); - - await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false); - await pump(const Duration(seconds: 5)); - } - - // For mobile platform to launch the app in anonymous mode - Future launchInAnonymousMode() async { - assert( - [TargetPlatform.android, TargetPlatform.iOS] - .contains(defaultTargetPlatform), - 'This method is only supported on mobile platforms', - ); - - await initializeAppFlowy(); - - final anonymousSignInButton = find.byType(SignInAnonymousButtonV2); - expect(anonymousSignInButton, findsOneWidget); - await tapButton(anonymousSignInButton); - - await pumpUntilFound(find.byType(MobileHomeScreen)); - } - - Future tapSvgButton(FlowySvgData svg) async { - final button = find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg.path == svg.path, - ); - await tapButton(button); - } - - Future openMoreViewActions() async { - final button = find.byType(MoreViewActions); - await tapButton(button); - } - - /// Presses on the Duplicate ViewAction in the [MoreViewActions] popup. - /// - /// [openMoreViewActions] must be called beforehand! - /// - Future duplicateByMoreViewActions() async { - final button = find.byWidgetPredicate( - (widget) => - widget is ViewAction && widget.type == ViewMoreActionType.duplicate, - ); - await tap(button); - await pump(); - } - - /// Presses on the Delete ViewAction in the [MoreViewActions] popup. - /// - /// [openMoreViewActions] must be called beforehand! - /// - Future deleteByMoreViewActions() async { - final button = find.descendant( - of: find.byType(ListView), - matching: find.byWidgetPredicate( - (widget) => - widget is ViewAction && widget.type == ViewMoreActionType.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 { - Finder findSettingsScrollable() => find - .descendant( - of: find - .descendant( - of: find.byType(SettingsBody), - matching: find.byType(SingleChildScrollView), - ) - .first, - matching: find.byType(Scrollable), - ) - .first; - - Finder findSettingsMenuScrollable() => find - .descendant( - of: find - .descendant( - of: find.byType(SettingsMenu), - matching: find.byType(SingleChildScrollView), - ) - .first, - matching: find.byType(Scrollable), - ) - .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) { - case ViewLayoutPB.Grid: - return LocaleKeys.grid_menuName.tr(); - case ViewLayoutPB.Board: - return LocaleKeys.board_menuName.tr(); - case ViewLayoutPB.Document: - return LocaleKeys.document_menuName.tr(); - case ViewLayoutPB.Calendar: - return LocaleKeys.calendar_menuName.tr(); - default: - throw UnsupportedError('Unsupported layout: $this'); - } - } - - String get referencedMenuName { - switch (this) { - case ViewLayoutPB.Grid: - return LocaleKeys.document_plugins_referencedGrid.tr(); - case ViewLayoutPB.Board: - return LocaleKeys.document_plugins_referencedBoard.tr(); - case ViewLayoutPB.Calendar: - return LocaleKeys.document_plugins_referencedCalendar.tr(); - default: - 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/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart deleted file mode 100644 index 970965f294..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ /dev/null @@ -1,1788 +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/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'; -import 'package:appflowy/plugins/database/calendar/presentation/calendar_day.dart'; -import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_card.dart'; -import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart'; -import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; -import 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart'; -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart'; -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/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/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'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_editor.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_menu.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart'; -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'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart'; -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/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'; -import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; -import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart'; -import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_document.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; -import 'package:appflowy/plugins/database/widgets/setting/database_layout_selector.dart'; -import 'package:appflowy/plugins/database/widgets/setting/database_setting_action.dart'; -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/pop_up_action.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.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:calendar_view/calendar_view.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: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 -import 'package:table_calendar/src/widgets/cell_content.dart'; -import 'package:table_calendar/table_calendar.dart'; - -import 'base.dart'; -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 { - final context = await initializeAppFlowy(); - await tapAnonymousSignInButton(); - - // expect to see a readme page - expectToSeePageName(gettingStarted); - - 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"); - - // Write the content to the file. - final path = p.join( - context.applicationDataDirectory, - fileName, - ); - final pageName = p.basenameWithoutExtension(path); - File(path).writeAsStringSync(str); - // mock get files - mockPickFilePaths( - paths: [path], - ); - await tapDatabaseRawDataButton(); - await openPage(pageName, layout: ViewLayoutPB.Grid); - } - - Future hoverOnFirstRowOfGrid([Future Function()? onHover]) async { - final findRow = find.byType(GridRow); - expect(findRow, findsWidgets); - - final firstRow = findRow.first; - await hoverOnWidget(firstRow, onHover: onHover); - } - - Future editCell({ - required int rowIndex, - required FieldType fieldType, - required String input, - int cellIndex = 0, - }) async { - final cell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex); - - expect(cell, findsOneWidget); - await enterText(cell, input); - await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(); - } - - Finder cellFinder(int rowIndex, FieldType fieldType, {int cellIndex = 0}) { - final findRow = find.byType(GridRow, skipOffstage: false); - final findCell = finderForFieldType(fieldType); - return find - .descendant( - of: findRow.at(rowIndex), - matching: findCell, - skipOffstage: false, - ) - .at(cellIndex); - } - - Future tapCheckboxCellInGrid({ - required int rowIndex, - }) async { - final cell = cellFinder(rowIndex, FieldType.Checkbox); - - final button = find.descendant( - of: cell, - matching: find.byType(FlowyIconButton), - ); - - expect(cell, findsOneWidget); - await tapButton(button); - } - - Future assertCheckboxCell({ - required int rowIndex, - required bool isSelected, - }) async { - final cell = cellFinder(rowIndex, FieldType.Checkbox); - final finder = isSelected - ? find.byWidgetPredicate( - (widget) => - widget is FlowySvg && widget.svg == FlowySvgs.check_filled_s, - ) - : find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s, - ); - - expect( - find.descendant( - of: cell, - matching: finder, - ), - findsOneWidget, - ); - } - - Future tapCellInGrid({ - required int rowIndex, - required FieldType fieldType, - }) async { - final cell = cellFinder(rowIndex, fieldType); - expect(cell, findsOneWidget); - await tapButton(cell); - } - - /// The [fieldName] must be unique in the grid. - void assertCellContent({ - required int rowIndex, - required FieldType fieldType, - required String content, - int cellIndex = 0, - }) { - final findCell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex); - final findContent = find.descendant( - of: findCell, - matching: find.text(content), - skipOffstage: false, - ); - expect(findContent, findsOneWidget); - } - - Future assertSingleSelectOption({ - required int rowIndex, - required String content, - }) async { - final findCell = cellFinder(rowIndex, FieldType.SingleSelect); - if (content.isNotEmpty) { - final finder = find.descendant( - of: findCell, - matching: find.byWidgetPredicate( - (widget) => - widget is SelectOptionTag && - (widget.name == content || widget.option?.name == content), - ), - ); - expect(finder, findsOneWidget); - } - } - - void assertMultiSelectOption({ - required int rowIndex, - required List contents, - }) { - final findCell = cellFinder(rowIndex, FieldType.MultiSelect); - for (final content in contents) { - if (content.isNotEmpty) { - final finder = find.descendant( - of: findCell, - matching: find.byWidgetPredicate( - (widget) => - widget is SelectOptionTag && - (widget.name == content || widget.option?.name == content), - ), - ); - expect(finder, findsOneWidget); - } - } - } - - /// null percent means no progress bar should be found - void assertChecklistCellInGrid({ - required int rowIndex, - required double? percent, - }) { - final findCell = cellFinder(rowIndex, FieldType.Checklist); - - if (percent == null) { - final finder = find.descendant( - of: findCell, - matching: find.byType(ChecklistProgressBar), - ); - expect(finder, findsNothing); - } else { - final finder = find.descendant( - of: findCell, - matching: find.byWidgetPredicate( - (widget) => - widget is ChecklistProgressBar && widget.percent == percent, - ), - ); - expect(finder, findsOneWidget); - } - } - - Future selectDay({ - required int content, - }) async { - final findCalendar = find.byType(TableCalendar); - final findDay = find.text(content.toString()); - - final finder = find.descendant( - of: findCalendar, - matching: findDay, - ); - - // if the day is very near the beginning or the end of the month, - // it may overlap with the same day in the next or previous month, - // respectively because it was spilling over. This will lead to 2 - // widgets being found and thus cannot be tapped correctly. - if (content < 15) { - // e.g., Jan 2 instead of Feb 2 - await tapButton(finder.first); - } else { - // e.g. Jun 28 instead of May 28 - await tapButton(finder.last); - } - } - - Future toggleIncludeTime() async { - final findDateEditor = find.byType(IncludeTimeButton); - final findToggle = find.byType(Toggle); - final finder = find.descendant( - of: findDateEditor, - matching: findToggle, - ); - await tapButton(finder); - } - - Future selectReminderOption(ReminderOption option) async { - await tapButton(find.byType(ReminderSelector)); - - final finder = find.descendant( - of: find.byType(FlowyButton), - matching: find.textContaining(option.label), - ); - - await tapButton(finder); - } - - Future selectLastDateInPicker() async { - final finder = find.byType(CellContent).last; - final w = widget(finder) as CellContent; - - await tapButton(finder); - - return w.isToday; - } - - Future tapChangeDateTimeFormatButton() async { - await tapButton(find.byType(DateTypeOptionButton)); - } - - Future changeDateFormat() async { - final findDateFormatButton = find.byType(DateFormatButton); - await tapButton(findDateFormatButton); - - final findNewDateFormat = find.text("Day/Month/Year"); - await tapButton(findNewDateFormat); - } - - Future changeTimeFormat() async { - final findDateFormatButton = find.byType(TimeFormatButton); - await tapButton(findDateFormatButton); - - final findNewDateFormat = find.text("12 hour"); - await tapButton(findNewDateFormat); - } - - Future clearDate() async { - final findDateEditor = find.byType(DateCellEditor); - final findClearButton = find.byType(ClearDateButton); - final finder = find.descendant( - of: findDateEditor, - matching: findClearButton, - ); - await tapButton(finder); - } - - Future tapSelectOptionCellInGrid({ - required int rowIndex, - required FieldType fieldType, - }) async { - assert( - fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect, - ); - - final findRow = find.byType(GridRow); - final findCell = finderForFieldType(fieldType); - - final cell = find.descendant( - of: findRow.at(rowIndex), - matching: findCell, - ); - - await tapButton(cell); - } - - /// The [SelectOptionCellEditor] must be opened first. - Future createOption({required String name}) async { - final findEditor = find.byType(SelectOptionCellEditor); - expect(findEditor, findsOneWidget); - - final findTextField = find.byType(SelectOptionTextField); - expect(findTextField, findsOneWidget); - - await enterText(findTextField, name); - await pump(); - - await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(); - } - - 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, - ), - ); - - await tapButton(option); - } - - void findSelectOptionWithNameInGrid({ - required int rowIndex, - required String name, - }) { - final findRow = find.byType(GridRow); - final option = find.byWidgetPredicate( - (widget) => - widget is SelectOptionTag && - (widget.name == name || widget.option?.name == name), - ); - - final cell = find.descendant(of: findRow.at(rowIndex), matching: option); - expect(cell, findsOneWidget); - } - - void assertNumberOfSelectedOptionsInGrid({ - required int rowIndex, - required Matcher matcher, - }) { - final findRow = find.byType(GridRow); - - final options = find.byWidgetPredicate( - (widget) => widget is SelectOptionTag, - ); - - final cell = find.descendant(of: findRow.at(rowIndex), matching: options); - expect(cell, matcher); - } - - Future tapChecklistCellInGrid({required int rowIndex}) async { - final findRow = find.byType(GridRow); - final findCell = finderForFieldType(FieldType.Checklist); - - final cell = find.descendant(of: findRow.at(rowIndex), matching: findCell); - await tapButton(cell); - } - - void assertChecklistEditorVisible({required bool visible}) { - final editor = find.byType(ChecklistCellEditor); - if (visible) { - return expect(editor, findsOneWidget); - } - expect(editor, findsNothing); - } - - Future createNewChecklistTask({ - required String name, - enter = false, - button = false, - }) async { - assert(!(enter && button)); - final textField = find.descendant( - of: find.byType(NewTaskItem), - matching: find.byType(TextField), - ); - - await enterText(textField, name); - await pumpAndSettle(); - if (enter) { - await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(const Duration(milliseconds: 500)); - } else { - await tapButton( - find.descendant( - of: find.byType(NewTaskItem), - matching: find.byType(FlowyTextButton), - ), - ); - } - } - - void assertChecklistTaskInEditor({ - required int index, - required String name, - required bool isChecked, - }) { - final task = find.byType(ChecklistItem).at(index); - final widget = this.widget(task); - assert( - widget.task.data.name == name && widget.task.isSelected == isChecked, - ); - } - - Future renameChecklistTask({ - required int index, - required String name, - bool enter = true, - }) async { - final textField = find - .descendant( - of: find.byType(ChecklistItem), - matching: find.byType(TextField), - ) - .at(index); - - await enterText(textField, name); - if (enter) { - await testTextInput.receiveAction(TextInputAction.done); - } - await pumpAndSettle(); - } - - Future checkChecklistTask({required int index}) async { - final button = find.descendant( - of: find.byType(ChecklistItem).at(index), - matching: find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s, - ), - ); - - await tapButton(button); - } - - 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); - }, - ); - } - - 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 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); - } - - Future openFirstRowDetailPage() async { - await hoverOnFirstRowOfGrid(); - - final expandButton = find.byType(PrimaryCellAccessory); - expect(expandButton, findsOneWidget); - await tapButton(expandButton); - } - - void assertRowDetailPageOpened() async { - final findRowDetailPage = find.byType(RowDetailPage); - expect(findRowDetailPage, findsOneWidget); - } - - Future dismissRowDetailPage() async { - // use tap empty area instead of clicking ESC to dismiss the row detail page - // sometimes, the ESC key is not working. - await simulateKeyEvent(LogicalKeyboardKey.escape); - await pumpAndSettle(); - final findRowDetailPage = find.byType(RowDetailPage); - if (findRowDetailPage.evaluate().isNotEmpty) { - await tapAt(const Offset(0, 0)); - await pumpAndSettle(); - } - } - - Future hoverRowBanner() async { - final banner = find.byType(RowBanner); - expect(banner, findsOneWidget); - - await startGesture( - getCenter(banner) + const Offset(0, -10), - 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())); - - Future tapDateCellInRowDetailPage() async { - final findDateCell = find.byType(EditableDateCell); - await tapButton(findDateCell); - } - - Future tapGridFieldWithNameInRowDetailPage(String name) async { - final fields = find.byWidgetPredicate( - (widget) => widget is FieldCellButton && widget.field.name == name, - ); - final field = find.descendant( - of: find.byType(RowDetailPage), - matching: fields, - ); - await tapButton(field); - await pumpAndSettle(); - } - - Future hoverOnFieldInRowDetail({required int index}) async { - final fieldButtons = find.byType(FieldCellButton); - final button = find - .descendant(of: find.byType(RowDetailPage), matching: fieldButtons) - .at(index); - return startGesture(getCenter(button), kind: PointerDeviceKind.mouse); - } - - Future reorderFieldInRowDetail({required double offset}) async { - final thumb = find - .byWidgetPredicate( - (widget) => widget is ReorderableDragStartListener && widget.enabled, - ) - .first; - await drag(thumb, Offset(0, offset), kind: PointerDeviceKind.mouse); - await pumpAndSettle(); - } - - void assertToggleShowHiddenFieldsVisibility(bool shown) { - final button = find.byType(ToggleHiddenFieldsVisibilityButton); - if (shown) { - expect(button, findsOneWidget); - } else { - expect(button, findsNothing); - } - } - - Future toggleShowHiddenFields() async { - final button = find.byType(ToggleHiddenFieldsVisibilityButton); - await tapButton(button); - } - - Future tapDeletePropertyInFieldEditor() async { - final deleteButton = find.byWidgetPredicate( - (w) => w is FieldActionCell && w.action == FieldAction.delete, - ); - await tapButton(deleteButton); - await tapButtonWithName(LocaleKeys.space_delete.tr()); - } - - Future scrollRowDetailByOffset(Offset offset) async { - await drag(find.byType(RowDetailPage), offset); - await pumpAndSettle(); - } - - Future scrollToRight(Finder find) async { - final size = getSize(find); - await drag(find, Offset(-size.width, 0), warnIfMissed: false); - await pumpAndSettle(const Duration(milliseconds: 500)); - } - - Future tapNewPropertyButton() async { - await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr()); - await pumpAndSettle(); - } - - Future tapGridFieldWithName(String name) async { - final field = find.byWidgetPredicate( - (widget) => widget is FieldCellButton && widget.field.name == name, - ); - await tapButton(field); - await pumpAndSettle(); - } - - Future changeFieldTypeOfFieldWithName( - String name, - FieldType type, { - ViewLayoutPB layout = ViewLayoutPB.Grid, - }) async { - await tapGridFieldWithName(name); - if (layout == ViewLayoutPB.Grid) { - await tapEditFieldButton(); - } - - await tapSwitchFieldTypeButton(); - await selectFieldType(type); - 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(); - - await tap( - find.descendant( - of: find.byType(CalculationTypeItem), - matching: find.text(type.label), - ), - ); - await pumpAndSettle(); - } - - /// Should call [tapGridFieldWithName] first. - Future tapEditFieldButton() async { - await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr()); - await pumpAndSettle(const Duration(milliseconds: 200)); - } - - /// Should call [tapGridFieldWithName] first. - Future tapDeletePropertyButton() async { - final field = find.byWidgetPredicate( - (w) => w is FieldActionCell && w.action == FieldAction.delete, - ); - await tapButton(field); - } - - /// A SimpleDialog must be shown first, e.g. when deleting a field. - Future tapDialogOkButton() async { - final field = find.byWidgetPredicate( - (w) => w is PrimaryTextButton && w.label == LocaleKeys.button_ok.tr(), - ); - await tapButton(field); - } - - /// Should call [tapGridFieldWithName] first. - Future tapDuplicatePropertyButton() async { - final field = find.byWidgetPredicate( - (w) => w is FieldActionCell && w.action == FieldAction.duplicate, - ); - await tapButton(field); - } - - Future tapInsertFieldButton({ - required bool left, - required String name, - }) async { - final field = find.byWidgetPredicate( - (widget) => - widget is FieldActionCell && - (left && widget.action == FieldAction.insertLeft || - !left && widget.action == FieldAction.insertRight), - ); - await tapButton(field); - await renameField(name); - } - - /// Should call [tapGridFieldWithName] first. - Future tapHidePropertyButton() async { - final field = find.byWidgetPredicate( - (w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility, - ); - await tapButton(field); - } - - Future tapHidePropertyButtonInFieldEditor() async { - final button = find.byWidgetPredicate( - (w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility, - ); - await tapButton(button); - } - - Future tapRowDetailPageRowActionButton() async => - tapButton(find.byType(RowActionButton)); - - Future tapRowDetailPageCreatePropertyButton() async => - tapButton(find.byType(CreateRowFieldButton)); - - Future tapRowDetailPageDeleteRowButton() async => - tapButton(find.byType(RowDetailPageDeleteButton)); - - Future tapRowDetailPageDuplicateRowButton() async => - tapButton(find.byType(RowDetailPageDuplicateButton)); - - Future tapSwitchFieldTypeButton() async => - tapButton(find.byType(SwitchFieldButton)); - - Future tapEscButton() async => sendKeyEvent(LogicalKeyboardKey.escape); - - /// Must call [tapSwitchFieldTypeButton] first. - Future selectFieldType(FieldType fieldType) async { - final fieldTypeCell = find.byType(FieldTypeCell); - final fieldTypeButton = find.descendant( - of: fieldTypeCell, - matching: find.byWidgetPredicate( - (widget) => widget is FlowyText && widget.text == fieldType.i18n, - ), - ); - await tapButton(fieldTypeButton); - } - - // Use in edit mode of FieldEditor - void expectEmptyTypeOptionEditor() => expect( - find.descendant( - of: find.byType(FieldTypeOptionEditor), - matching: find.byType(TypeOptionSeparator), - ), - findsNothing, - ); - - /// 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) { - final finder = finderForFieldType(fieldType); - expect(finder, findsWidgets); - } - - void assertNumberOfRowsInGridPage(int num) { - expect( - find.byType(GridRow, skipOffstage: false), - findsNWidgets(num), - ); - } - - Future assertDocumentExistInRowDetailPage() async { - expect(find.byType(RowDocument), findsOneWidget); - } - - /// Check the field type of the [FieldCellButton] is the same as the name. - Future assertFieldTypeWithFieldName(String name, FieldType type) async { - final field = find.byWidgetPredicate( - (widget) => - widget is FieldCellButton && - widget.field.fieldType == type && - widget.field.name == name, - ); - - expect(field, findsOneWidget); - } - - void assertFirstFieldInRowDetailByType(FieldType fieldType) { - final firstField = find - .descendant( - of: find.byType(RowDetailPage), - matching: find.byType(FieldCellButton), - ) - .first; - - final widget = this.widget(firstField); - expect(widget.field.fieldType, fieldType); - } - - void findFieldWithName(String name) { - final field = find.byWidgetPredicate( - (widget) => widget is FieldCellButton && widget.field.name == name, - ); - expect(field, findsOneWidget); - } - - void noFieldWithName(String name) { - final field = find.byWidgetPredicate( - (widget) => widget is FieldCellButton && widget.field.name == name, - ); - expect(field, findsNothing); - } - - Future renameField(String newName) async { - final textField = find.byType(FieldNameTextField); - expect(textField, findsOneWidget); - await enterText(textField, newName); - await pumpAndSettle(); - } - - Future dismissFieldEditor() async { - await sendKeyEvent(LogicalKeyboardKey.escape); - 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); - } - - Future dismissCellEditor() async { - await sendKeyEvent(LogicalKeyboardKey.escape); - await pumpAndSettle(); - } - - Future tapCreateRowButtonInGrid() async { - await tapButton(find.byType(GridAddRowButton)); - } - - Future tapCreateRowButtonAfterHoveringOnGridRow() async { - await tapButton(find.byType(InsertRowButton)); - } - - Future tapRowMenuButtonInGrid() async { - 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 { - 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, - ViewLayoutPB layout = ViewLayoutPB.Grid, - }) async { - if (layout == ViewLayoutPB.Grid) { - await scrollToRight(find.byType(GridPage)); - } - await tapNewPropertyButton(); - if (name != null) { - await renameField(name); - } - await tapSwitchFieldTypeButton(); - await selectFieldType(fieldType); - } - - Future tapDatabaseSettingButton() async { - await tapButton(find.byType(SettingButton)); - } - - Future tapDatabaseFilterButton() async { - await tapButton(find.byType(FilterButton)); - } - - Future tapDatabaseSortButton() async { - await tapButton(find.byType(SortButton)); - } - - Future tapCreateFilterByFieldType(FieldType type, String title) async { - final findFilter = find.byWidgetPredicate( - (widget) => - widget is FilterableFieldButton && - widget.fieldInfo.fieldType == type && - widget.fieldInfo.name == title, - ); - await tapButton(findFilter); - } - - Future tapFilterButtonInGrid(String name) async { - final button = find.byWidgetPredicate( - (widget) => widget is ChoiceChipButton && widget.fieldInfo.name == name, - ); - await tapButton(button); - } - - Future tapCreateSortByFieldType(FieldType type, String title) async { - final findSort = find.byWidgetPredicate( - (widget) => - widget is GridSortPropertyCell && - widget.fieldInfo.fieldType == type && - widget.fieldInfo.name == title, - ); - await tapButton(findSort); - } - - // Must call [tapSortMenuInSettingBar] first. - Future tapCreateSortByFieldTypeInSortMenu( - FieldType fieldType, - String title, - ) async { - await tapButton(find.byType(DatabaseAddSortButton)); - - final findSort = find.byWidgetPredicate( - (widget) => - widget is GridSortPropertyCell && - widget.fieldInfo.fieldType == fieldType && - widget.fieldInfo.name == title, - ); - - await tapButton(findSort); - await pumpAndSettle(); - } - - Future tapSortMenuInSettingBar() async { - await tapButton(find.byType(SortMenu)); - await pumpAndSettle(); - } - - /// 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), - ); - await tapButton(sortItem); - } - - /// Must call [tapSortMenuInSettingBar] first. - Future reorderSort( - (FieldType, String) from, - (FieldType, String) to, - ) async { - final fromSortItem = find.ancestor( - of: find.text(from.$2), - matching: find.byType(DatabaseSortItem), - ); - final toSortItem = find.ancestor( - of: find.text(to.$2), - matching: find.byType(DatabaseSortItem), - ); - // 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), - ); - await drag(dragElement, getCenter(toSortItem) - getCenter(fromSortItem)); - await pumpAndSettle(const Duration(milliseconds: 200)); - } - - /// Must call [tapEditSortConditionButtonByFieldName] first. - Future tapSortByDescending() async { - await tapButton( - find.byWidgetPredicate( - (widget) => - widget is OrderPanelItem && - widget.condition == SortConditionPB.Descending, - ), - ); - await sendKeyEvent(LogicalKeyboardKey.escape); - await pumpAndSettle(); - } - - /// Must call [tapSortMenuInSettingBar] first. - Future tapDeleteAllSortsButton() async { - await tapButton(find.byType(DeleteAllSortsButton)); - } - - Future scrollOptionFilterListByOffset(Offset offset) async { - await drag(find.byType(SelectOptionFilterEditor), offset); - await pumpAndSettle(); - } - - Future enterTextInTextFilter(String text) async { - final findEditor = find.byType(TextFilterEditor); - final findTextField = find.descendant( - of: findEditor, - matching: find.byType(FlowyTextField), - ); - - await enterText(findTextField, text); - await pumpAndSettle(const Duration(milliseconds: 300)); - } - - Future tapDisclosureButtonInFinder(Finder finder) async { - final findDisclosure = find.descendant( - of: finder, - matching: find.byType(DisclosureButton), - ); - - await tapButton(findDisclosure); - } - - /// must call [tapDisclosureButtonInFinder] first. - Future tapDeleteFilterButtonInGrid() async { - await tapButton(find.text(LocaleKeys.grid_settings_deleteFilter.tr())); - } - - Future tapCheckboxFilterButtonInGrid() async { - await tapButton(find.byType(CheckboxFilterConditionList)); - } - - Future tapChecklistFilterButtonInGrid() async { - await tapButton(find.byType(ChecklistFilterConditionList)); - } - - /// The [SelectOptionFilterList] must show up first. - Future tapOptionFilterWithName(String name) async { - final findCell = find.descendant( - of: find.byType(SelectOptionFilterList), - matching: find.byWidgetPredicate( - (widget) => - widget is SelectOptionFilterCell && widget.option.name == name, - skipOffstage: false, - ), - skipOffstage: false, - ); - expect(findCell, findsOneWidget); - await tapButton(findCell); - } - - Future tapUnCheckedButtonOnCheckboxFilter() async { - final button = find.descendant( - of: find.byType(HoverButton), - matching: find.text(LocaleKeys.grid_checkboxFilter_isUnchecked.tr()), - ); - - await tapButton(button); - } - - Future tapCompletedButtonOnChecklistFilter() async { - final button = find.descendant( - of: find.byType(HoverButton), - matching: find.text(LocaleKeys.grid_checklistFilter_isComplete.tr()), - ); - - 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); - final findLayoutButton = find.byWidgetPredicate( - (widget) => - widget is FlowyText && - widget.text == DatabaseSettingAction.showProperties.title(), - ); - - final button = find.descendant( - of: findSettingItem, - matching: findLayoutButton, - ); - - await tapButton(button); - } - - /// Should call [tapDatabaseSettingButton] first. - Future tapDatabaseLayoutButton() async { - final findSettingItem = find.byType(DatabaseSettingsList); - final findLayoutButton = find.byWidgetPredicate( - (widget) => - widget is FlowyText && - widget.text == DatabaseSettingAction.showLayout.title(), - ); - - final button = find.descendant( - of: findSettingItem, - matching: findLayoutButton, - ); - - await tapButton(button); - } - - Future tapCalendarLayoutSettingButton() async { - final findSettingItem = find.byType(DatabaseSettingsList); - final findLayoutButton = find.byWidgetPredicate( - (widget) => - widget is FlowyText && - widget.text == DatabaseSettingAction.showCalendarLayout.title(), - ); - - final button = find.descendant( - of: findSettingItem, - matching: findLayoutButton, - ); - - await tapButton(button); - } - - Future tapFirstDayOfWeek() async => - tapButton(find.byType(FirstDayOfWeek)); - - Future tapFirstDayOfWeekStartFromMonday() async { - final finder = find.byWidgetPredicate( - (widget) => widget is StartFromButton && widget.dayIndex == 1, - ); - await tapButton(finder); - - // Dismiss the popover overlay in cause of obscure the tapButton - // in the next test case. - await sendKeyEvent(LogicalKeyboardKey.escape); - await pumpAndSettle(const Duration(milliseconds: 200)); - } - - void assertFirstDayOfWeekStartFromMonday() { - final finder = find.byWidgetPredicate( - (w) => w is StartFromButton && w.dayIndex == 1 && w.isSelected == true, - ); - expect(finder, findsOneWidget); - } - - void assertFirstDayOfWeekStartFromSunday() { - final finder = find.byWidgetPredicate( - (w) => w is StartFromButton && w.dayIndex == 0 && w.isSelected == true, - ); - expect(finder, findsOneWidget); - } - - Future scrollToToday() async { - final todayCell = find.byWidgetPredicate( - (widget) => widget is CalendarDayCard && widget.isToday, - ); - final scrollable = find - .descendant( - of: find.byType(MonthView), - matching: find.byWidgetPredicate( - (widget) => widget is Scrollable && widget.axis == Axis.vertical, - ), - ) - .first; - await scrollUntilVisible(todayCell, 300, scrollable: scrollable); - await pumpAndSettle(const Duration(milliseconds: 300)); - } - - Future hoverOnTodayCalendarCell({ - Future Function()? onHover, - }) async { - final todayCell = find.byWidgetPredicate( - (widget) => widget is CalendarDayCard && widget.isToday, - ); - - await hoverOnWidget(todayCell, onHover: onHover); - } - - Future tapAddCalendarEventButton() async { - final findFlowyButton = find.byType(FlowyIconButton); - final findNewEventButton = find.byType(NewEventButton); - final button = find.descendant( - of: findNewEventButton, - matching: findFlowyButton, - ); - await tapButton(button); - } - - /// Checks for a certain number of events. Parameters [date] and [title] can - /// also be provided to restrict the scope of the search - void assertNumberOfEventsInCalendar(int number, {String? title}) { - Finder findEvents = find.byType(EventCard); - if (title != null) { - findEvents = find.descendant(of: findEvents, matching: find.text(title)); - } - expect(findEvents, findsNWidgets(number)); - } - - void assertNumberOfEventsOnSpecificDay( - int number, - DateTime date, { - String? title, - }) { - final findDayCell = find.byWidgetPredicate( - (widget) => widget is CalendarDayCard && isSameDay(widget.date, date), - ); - Finder findEvents = find.descendant( - of: findDayCell, - matching: find.byType(EventCard), - ); - if (title != null) { - findEvents = find.descendant(of: findEvents, matching: find.text(title)); - } - expect(findEvents, findsNWidgets(number)); - } - - Future doubleClickCalendarCell(DateTime date) async { - final todayCell = find.byWidgetPredicate( - (widget) => widget is CalendarDayCard && isSameDay(date, widget.date), - ); - final location = getTopLeft(todayCell).translate(10, 10); - await doubleTapAt(location); - } - - Future openCalendarEvent({required int index, DateTime? date}) async { - final findDayCell = find.byWidgetPredicate( - (widget) => - widget is CalendarDayCard && - isSameDay(widget.date, date ?? DateTime.now()), - ); - final cards = find.descendant( - of: findDayCell, - matching: find.byType(EventCard), - ); - - await tapButton(cards.at(index), milliseconds: 1000); - } - - void assertEventEditorOpen() => - expect(find.byType(CalendarEventEditor), findsOneWidget); - - Future dismissEventEditor() async => - simulateKeyEvent(LogicalKeyboardKey.escape); - - Future editEventTitle(String title) async { - final textField = find.descendant( - of: find.byType(CalendarEventEditor), - matching: find.byType(FlowyTextField), - ); - - await enterText(textField, title); - await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(const Duration(milliseconds: 300)); - } - - Future openEventToRowDetailPage() async { - final button = find.descendant( - of: find.byType(CalendarEventEditor), - matching: find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.full_view_s, - ), - ); - - await tapButton(button); - } - - Future deleteEventFromEventEditor() async { - final button = find.descendant( - of: find.byType(CalendarEventEditor), - matching: find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s, - ), - ); - - await tapButton(button); - await tapButtonWithName(LocaleKeys.button_delete.tr()); - } - - Future dragDropRescheduleCalendarEvent() async { - final findEventCard = find.byType(EventCard); - await drag(findEventCard.first, const Offset(0, 300)); - await pumpAndSettle(const Duration(microseconds: 300)); - } - - Future openUnscheduledEventsPopup() async { - final button = find.byType(UnscheduledEventsButton); - await tapButton(button); - } - - void findUnscheduledPopup(Matcher matcher, int numUnscheduledEvents) { - expect(find.byType(UnscheduleEventsList), matcher); - if (matcher != findsNothing) { - expect( - find.byType(UnscheduledEventCell), - findsNWidgets(numUnscheduledEvents), - ); - } - } - - Future clickUnscheduledEvent() async { - final unscheduledEvent = find.byType(UnscheduledEventCell); - await tapButton(unscheduledEvent); - } - - Future tapCreateLinkedDatabaseViewButton( - DatabaseLayoutPB layoutType, - ) async { - final findAddButton = find.byType(AddDatabaseViewButton); - await tapButton(findAddButton); - - final findCreateButton = find.byWidgetPredicate( - (widget) => - widget is TabBarAddButtonActionCell && widget.action == layoutType, - ); - await tapButton(findCreateButton); - } - - void assertNumberOfGroups(int number) { - final groups = find.byType(BoardColumnHeader, skipOffstage: false); - expect(groups, findsNWidgets(number)); - } - - Future scrollBoardToEnd() async { - final scrollable = find - .descendant( - of: find.byType(AppFlowyBoard), - matching: find.byWidgetPredicate( - (widget) => widget is Scrollable && widget.axis == Axis.horizontal, - ), - ) - .first; - await scrollUntilVisible( - find.byType(BoardTrailing), - 300, - scrollable: scrollable, - ); - } - - Future tapNewGroupButton() async { - final button = find.descendant( - of: find.byType(BoardTrailing), - matching: find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s, - ), - ); - expect(button, findsOneWidget); - await tapButton(button); - } - - void assertNewGroupTextField(bool isVisible) { - final textField = find.descendant( - of: find.byType(BoardTrailing), - matching: find.byType(TextField), - ); - if (isVisible) { - return expect(textField, findsOneWidget); - } - expect(textField, findsNothing); - } - - Future enterNewGroupName(String name, {required bool submit}) async { - final textField = find.descendant( - of: find.byType(BoardTrailing), - matching: find.byType(TextField), - ); - await enterText(textField, name); - await pumpAndSettle(); - if (submit) { - await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(); - } - } - - Future clearNewGroupTextField() async { - final textField = find.descendant( - of: find.byType(BoardTrailing), - matching: find.byType(TextField), - ); - await tapButton( - find.descendant( - of: textField, - matching: find.byWidgetPredicate( - (widget) => - widget is FlowySvg && widget.svg == FlowySvgs.close_filled_s, - ), - ), - ); - final textFieldWidget = widget(textField); - assert( - textFieldWidget.controller != null && - textFieldWidget.controller!.text.isEmpty, - ); - } - - Future tapTabBarLinkedViewByViewName(String name) async { - final viewButton = findTabBarLinkViewByViewName(name); - await tapButton(viewButton); - } - - Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) { - return find.byWidgetPredicate( - (widget) => widget is TabBarItemButton && widget.view.layout == layout, - ); - } - - Finder findTabBarLinkViewByViewName(String name) { - return find.byWidgetPredicate( - (widget) => widget is TabBarItemButton && widget.view.name == name, - ); - } - - Future renameLinkedView(Finder linkedView, String name) async { - await tap(linkedView, buttons: kSecondaryButton); - await pumpAndSettle(); - - await tapButton( - find.byWidgetPredicate( - (widget) => - widget is ActionCellWidget && - widget.action == TabBarViewAction.rename, - ), - ); - - await enterText( - find.descendant( - of: find.byType(FlowyFormTextInput), - matching: find.byType(TextFormField), - ), - name, - ); - - final field = find.byWidgetPredicate( - (widget) => - widget is PrimaryTextButton && - widget.label == LocaleKeys.button_ok.tr(), - ); - await tapButton(field); - } - - Future deleteDatebaseView(Finder linkedView) async { - await tap(linkedView, buttons: kSecondaryButton); - await pumpAndSettle(); - - await tapButton( - find.byWidgetPredicate( - (widget) => - widget is ActionCellWidget && - widget.action == TabBarViewAction.delete, - ), - ); - - final okButton = find.byWidgetPredicate( - (widget) => - widget is PrimaryTextButton && - widget.label == LocaleKeys.button_ok.tr(), - ); - await tapButton(okButton); - } - - void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) { - DatabaseLayoutPB.Board => - expect(find.byType(DesktopBoardPage), findsOneWidget), - DatabaseLayoutPB.Calendar => - expect(find.byType(CalendarPage), findsOneWidget), - DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget), - _ => throw Exception('Unknown database layout type: $layout'), - }; - - Future selectDatabaseLayoutType(DatabaseLayoutPB layout) async { - final findLayoutCell = find.byType(DatabaseViewLayoutCell); - final findText = find.byWidgetPredicate( - (widget) => widget is FlowyText && widget.text == layout.layoutName, - ); - - final button = find.descendant(of: findLayoutCell, matching: findText); - await tapButton(button); - } - - Future assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async { - expect(finderForDatabaseLayoutType(layout), findsOneWidget); - } - - Future tapDatabaseRawDataButton() async { - await tapButtonWithName(LocaleKeys.importPanel_database.tr()); - } - - // Use in edit mode of FieldEditor - Future changeNumberFieldFormat() async { - final changeFormatButton = find.descendant( - of: find.byType(FieldTypeOptionEditor), - matching: find.text("Number"), - ); - await tapButton(changeFormatButton); - - await tapButton( - find.byWidgetPredicate( - (w) => w is NumberFormatCell && w.format == NumberFormatPB.USD, - ), - ); - } - - // Use in edit mode of FieldEditor - Future tapAddSelectOptionButton() async { - await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr()); - } - - Future tapViewTogglePropertyVisibilityButtonByName( - String fieldName, - ) async { - final field = find.byWidgetPredicate( - (w) => w is DatabasePropertyCell && w.fieldInfo.name == fieldName, - ); - final toggleVisibilityButton = - find.descendant(of: field, matching: find.byType(FlowyIconButton)); - await tapButton(toggleVisibilityButton); - } -} - -Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) { - DatabaseLayoutPB.Board => find.byType(DesktopBoardPage), - DatabaseLayoutPB.Calendar => find.byType(CalendarPage), - DatabaseLayoutPB.Grid => find.byType(GridPage), - _ => throw Exception('Unknown database layout type: $layout'), - }; - -Finder finderForFieldType(FieldType fieldType) { - switch (fieldType) { - case FieldType.Checkbox: - return find.byType(EditableCheckboxCell, skipOffstage: false); - case FieldType.DateTime: - return find.byType(EditableDateCell, skipOffstage: false); - case FieldType.LastEditedTime: - return find.byWidgetPredicate( - (widget) => - widget is EditableTimestampCell && - widget.fieldType == FieldType.LastEditedTime, - skipOffstage: false, - ); - case FieldType.CreatedTime: - return find.byWidgetPredicate( - (widget) => - widget is EditableTimestampCell && - widget.fieldType == FieldType.CreatedTime, - skipOffstage: false, - ); - case FieldType.SingleSelect: - return find.byWidgetPredicate( - (widget) => - widget is EditableSelectOptionCell && - widget.fieldType == FieldType.SingleSelect, - skipOffstage: false, - ); - case FieldType.MultiSelect: - return find.byWidgetPredicate( - (widget) => - widget is EditableSelectOptionCell && - widget.fieldType == FieldType.MultiSelect, - skipOffstage: false, - ); - case FieldType.Checklist: - return find.byType(EditableChecklistCell, skipOffstage: false); - case FieldType.Number: - return find.byType(EditableNumberCell, skipOffstage: false); - case FieldType.RichText: - 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/dir.dart b/frontend/appflowy_flutter/integration_test/shared/dir.dart deleted file mode 100644 index 56c6f302f0..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/dir.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:io'; - -import 'package:archive/archive.dart'; -import 'package:path/path.dart' as p; - -Future deleteDirectoriesWithSameBaseNameAsPrefix( - String path, -) async { - final dir = Directory(path); - final prefix = p.basename(dir.path); - final parentDir = dir.parent; - - // Check if the directory exists - if (!await parentDir.exists()) { - // ignore: avoid_print - print('Directory does not exist'); - return; - } - - // List all entities in the directory - await for (final entity in parentDir.list()) { - // Check if the entity is a directory and starts with the specified prefix - if (entity is Directory && p.basename(entity.path).startsWith(prefix)) { - try { - await entity.delete(recursive: true); - } catch (e) { - // ignore: avoid_print - print('Failed to delete directory: ${entity.path}, Error: $e'); - } - } - } -} - -Future unzipFile(File zipFile, Directory targetDirectory) async { - // Read the Zip file from disk. - final bytes = zipFile.readAsBytesSync(); - - // Decode the Zip file - final archive = ZipDecoder().decodeBytes(bytes); - - // Extract the contents of the Zip archive to disk. - for (final file in archive) { - final filename = file.name; - if (file.isFile) { - final data = file.content as List; - File(p.join(targetDirectory.path, filename)) - ..createSync(recursive: true) - ..writeAsBytesSync(data); - } else { - Directory(p.join(targetDirectory.path, filename)) - .createSync(recursive: true); - } - } -} 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/emoji.dart b/frontend/appflowy_flutter/integration_test/shared/emoji.dart deleted file mode 100644 index cccd00a3f6..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/emoji.dart +++ /dev/null @@ -1,144 +0,0 @@ -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 { - final emojiWidget = find.descendant( - of: find.byType(EmojiPicker), - matching: find.text(emoji), - ); - 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 deleted file mode 100644 index 3b9ef0d75c..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ /dev/null @@ -1,347 +0,0 @@ -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/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'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart'; -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: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'; - -// const String readme = 'Read me'; -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 docFinder = find.textContaining(gettingStarted); - await pumpUntilFound(docFinder); - } - - Future expectToSeeHomePage() async { - final finder = find.byType(HomeStack); - await pumpUntilFound(finder); - expect(finder, findsOneWidget); - } - - /// Expect to see the page name on the home page. - void expectToSeePageName( - String name, { - String? parentName, - ViewLayoutPB layout = ViewLayoutPB.Document, - ViewLayoutPB parentLayout = ViewLayoutPB.Document, - }) { - final pageName = findPageName( - name, - layout: layout, - parentName: parentName, - parentLayout: parentLayout, - ); - expect(pageName, findsOneWidget); - } - - /// Expect not to see the page name on the home page. - void expectNotToSeePageName( - String name, { - String? parentName, - ViewLayoutPB layout = ViewLayoutPB.Document, - ViewLayoutPB parentLayout = ViewLayoutPB.Document, - }) { - final pageName = findPageName( - name, - layout: layout, - parentName: parentName, - parentLayout: parentLayout, - ); - expect(pageName, findsNothing); - } - - /// Expect to see the document banner. - void expectToSeeDocumentBanner() { - expect(find.byType(DocumentBanner), findsOneWidget); - } - - /// Expect not to see the document banner. - void expectNotToSeeDocumentBanner() { - expect(find.byType(DocumentBanner), findsNothing); - } - - /// Expect to the markdown file export success dialog. - void expectToExportSuccess() { - final exportSuccess = find.byWidgetPredicate( - (widget) => - widget is FlowyText && - widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(), - ); - expect(exportSuccess, findsOneWidget); - } - - /// Expect to see the document header toolbar empty - void expectToSeeEmptyDocumentHeaderToolbar() { - final addCover = find.textContaining( - LocaleKeys.document_plugins_cover_addCover.tr(), - ); - final addIcon = find.textContaining( - LocaleKeys.document_plugins_cover_addIcon.tr(), - ); - expect(addCover, findsNothing); - expect(addIcon, findsNothing); - } - - void expectToSeeDocumentIcon(String? emoji) { - if (emoji == null) { - final iconWidget = find.byType(EmojiIconWidget); - expect(iconWidget, findsNothing); - return; - } - final iconWidget = find.byWidgetPredicate( - (widget) => widget is EmojiIconWidget && widget.emoji.emoji == emoji, - ); - expect(iconWidget, findsOneWidget); - } - - void expectDocumentIconNotNull() { - final iconWidget = find.byWidgetPredicate( - (widget) => widget is EmojiIconWidget && widget.emoji.isNotEmpty, - ); - expect(iconWidget, findsOneWidget); - } - - void expectToSeeDocumentCover(CoverType type) { - final findCover = find.byWidgetPredicate( - (widget) => widget is DocumentCover && widget.coverType == type, - ); - expect(findCover, findsOneWidget); - } - - void expectToSeeNoDocumentCover() { - final findCover = find.byType(DocumentCover); - expect(findCover, findsNothing); - } - - void expectChangeCoverAndDeleteButton() { - final findChangeCover = find.text( - LocaleKeys.document_plugins_cover_changeCover.tr(), - ); - final findRemoveIcon = find.byType(DeleteCoverButton); - expect(findChangeCover, findsOneWidget); - expect(findRemoveIcon, findsOneWidget); - } - - /// Expect to see a text - void expectToSeeText(String text) { - Finder textWidget = find.textContaining(text, findRichText: true); - if (textWidget.evaluate().isEmpty) { - textWidget = find.byWidgetPredicate( - (widget) => widget is FlowyText && widget.text == text, - ); - } - expect(textWidget, findsOneWidget); - } - - /// Find if the page is favorite - Finder findFavoritePageName( - String name, { - ViewLayoutPB layout = ViewLayoutPB.Document, - String? parentName, - ViewLayoutPB parentLayout = ViewLayoutPB.Document, - }) => - find.byWidgetPredicate( - (widget) => - widget is SingleInnerViewItem && - widget.view.isFavorite && - widget.spaceType == FolderSpaceType.favorite && - widget.view.name == name && - widget.view.layout == layout, - skipOffstage: false, - ); - - Finder findAllFavoritePages() => find.byWidgetPredicate( - (widget) => - widget is SingleInnerViewItem && - widget.view.isFavorite && - widget.spaceType == FolderSpaceType.favorite, - ); - - Finder findPageName( - String name, { - ViewLayoutPB layout = ViewLayoutPB.Document, - String? parentName, - ViewLayoutPB parentLayout = ViewLayoutPB.Document, - }) { - if (UniversalPlatform.isDesktop) { - if (parentName == null) { - return find.byWidgetPredicate( - (widget) => - widget is SingleInnerViewItem && - widget.view.name == name && - widget.view.layout == layout, - skipOffstage: false, - ); - } - - return find.descendant( - of: find.byWidgetPredicate( - (widget) => - widget is InnerViewItem && - widget.view.name == parentName && - widget.view.layout == parentLayout, - skipOffstage: false, - ), - matching: findPageName(name, layout: layout), - ); - } - - return find.byWidgetPredicate( - (widget) => - widget is SingleMobileInnerViewItem && - widget.view.name == name && - widget.view.layout == layout, - skipOffstage: false, - ); - } - - void expectViewHasIcon(String name, ViewLayoutPB layout, EmojiIconData data) { - 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); - } - } - } - - 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 expectSelectedReminder(ReminderOption option) { - final findSelectedText = find.descendant( - of: find.byType(ReminderSelector), - matching: find.text(option.label), - ); - - expect(findSelectedText, findsOneWidget); - } - - void expectNotificationItems(int amount) { - final findItems = find.byType(NotificationItem); - - expect(findItems, findsNWidgets(amount)); - } - - void expectToSeeRowDetailsPageDialog() { - expect( - find.descendant( - of: find.byType(RowDetailPage), - matching: find.byType(SimpleDialog), - ), - findsOneWidget, - ); - } -} diff --git a/frontend/appflowy_flutter/integration_test/shared/ime.dart b/frontend/appflowy_flutter/integration_test/shared/ime.dart deleted file mode 100644 index e2b0a754b5..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/ime.dart +++ /dev/null @@ -1,47 +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'; - -extension IME on WidgetTester { - IMESimulator get ime => IMESimulator(this); -} - -class IMESimulator { - IMESimulator(this.tester) { - client = findTextInputClient(); - } - - final WidgetTester tester; - late final TextInputClient client; - - Future insertText(String text) async { - for (final c in text.characters) { - await insertCharacter(c); - } - } - - Future insertCharacter(String character) async { - final value = client.currentTextEditingValue; - if (value == null) { - assert(false); - return; - } - final text = value.text - .replaceRange(value.selection.start, value.selection.end, character); - final textEditingValue = TextEditingValue( - text: text, - selection: TextSelection.collapsed( - offset: value.selection.baseOffset + 1, - ), - ); - client.updateEditingValue(textEditingValue); - await tester.pumpAndSettle(); - } - - TextInputClient findTextInputClient() { - final finder = find.byType(KeyboardServiceWidget); - final KeyboardServiceWidgetState state = tester.state(finder); - return state.textInputService as TextInputClient; - } -} diff --git a/frontend/appflowy_flutter/integration_test/shared/keyboard.dart b/frontend/appflowy_flutter/integration_test/shared/keyboard.dart deleted file mode 100644 index 567e7e548c..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/keyboard.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart' as flutter_test; - -class FlowyTestKeyboard { - static Future simulateKeyDownEvent( - List keys, { - required flutter_test.WidgetTester tester, - bool withKeyUp = false, - }) async { - for (final LogicalKeyboardKey key in keys) { - await flutter_test.simulateKeyDownEvent(key); - await tester.pumpAndSettle(); - } - - if (withKeyUp) { - for (final LogicalKeyboardKey key in keys) { - await flutter_test.simulateKeyUpEvent(key); - await tester.pumpAndSettle(); - } - } - } -} diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart deleted file mode 100644 index 32a0c255d8..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; - -class MockFilePicker implements FilePickerService { - MockFilePicker({ - this.mockPath = '', - this.mockPaths = const [], - }); - - final String mockPath; - final List mockPaths; - - @override - Future getDirectoryPath({String? title}) => Future.value(mockPath); - - @override - Future saveFile({ - String? dialogTitle, - String? fileName, - String? initialDirectory, - FileType type = FileType.any, - List? allowedExtensions, - bool lockParentWindow = false, - }) => - Future.value(mockPath); - - @override - Future pickFiles({ - String? dialogTitle, - String? initialDirectory, - FileType type = FileType.any, - List? allowedExtensions, - Function(FilePickerStatus p1)? onFileLoading, - bool allowCompression = true, - bool allowMultiple = false, - bool withData = false, - bool withReadStream = false, - bool lockParentWindow = false, - }) { - final platformFiles = - mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList(); - return Future.value(FilePickerResult(platformFiles)); - } -} - -Future mockGetDirectoryPath(String path) async { - getIt.unregister(); - getIt.registerFactory( - () => MockFilePicker(mockPath: path), - ); -} - -Future mockSaveFilePath(String path) async { - getIt.unregister(); - getIt.registerFactory( - () => MockFilePicker(mockPath: path), - ); - return path; -} - -List mockPickFilePaths({required List paths}) { - getIt.unregister(); - getIt.registerFactory( - () => MockFilePicker(mockPaths: paths), - ); - return paths; -} diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart deleted file mode 100644 index b0281ab4b4..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2013 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:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:url_launcher_platform_interface/link.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; - -class MockUrlLauncher extends Fake - with MockPlatformInterfaceMixin - implements UrlLauncherPlatform { - String? url; - PreferredLaunchMode? launchMode; - bool? useSafariVC; - bool? useWebView; - bool? enableJavaScript; - bool? enableDomStorage; - bool? universalLinksOnly; - Map? headers; - String? webOnlyWindowName; - - bool? response; - - bool closeWebViewCalled = false; - bool canLaunchCalled = false; - bool launchCalled = false; - - // ignore: use_setters_to_change_properties - void setCanLaunchExpectations(String url) => this.url = url; - - void setLaunchExpectations({ - required String url, - PreferredLaunchMode? launchMode, - bool? useSafariVC, - bool? useWebView, - required bool enableJavaScript, - required bool enableDomStorage, - required bool universalLinksOnly, - required Map headers, - required String? webOnlyWindowName, - }) { - this.url = url; - this.launchMode = launchMode; - this.useSafariVC = useSafariVC; - this.useWebView = useWebView; - this.enableJavaScript = enableJavaScript; - this.enableDomStorage = enableDomStorage; - this.universalLinksOnly = universalLinksOnly; - this.headers = headers; - this.webOnlyWindowName = webOnlyWindowName; - } - - void setResponse(bool response) => this.response = response; - - @override - LinkDelegate? get linkDelegate => null; - - @override - Future canLaunch(String url) async { - expect(url, this.url); - canLaunchCalled = true; - return response!; - } - - @override - Future launch( - String url, { - required bool useSafariVC, - required bool useWebView, - required bool enableJavaScript, - required bool enableDomStorage, - required bool universalLinksOnly, - required Map headers, - String? webOnlyWindowName, - }) async { - expect(url, this.url); - expect(useSafariVC, this.useSafariVC); - expect(useWebView, this.useWebView); - expect(enableJavaScript, this.enableJavaScript); - expect(enableDomStorage, this.enableDomStorage); - expect(universalLinksOnly, this.universalLinksOnly); - expect(headers, this.headers); - expect(webOnlyWindowName, this.webOnlyWindowName); - launchCalled = true; - return response!; - } - - @override - Future launchUrl(String url, LaunchOptions options) async { - expect(url, this.url); - expect(options.mode, launchMode); - expect(options.webViewConfiguration.enableJavaScript, enableJavaScript); - expect(options.webViewConfiguration.enableDomStorage, enableDomStorage); - expect(options.webViewConfiguration.headers, headers); - expect(options.webOnlyWindowName, webOnlyWindowName); - launchCalled = true; - return response!; - } - - @override - Future closeWebView() async => closeWebViewCalled = true; -} diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart deleted file mode 100644 index bfc5efedde..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ /dev/null @@ -1,137 +0,0 @@ -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_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 '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); - - expect(settingsDialog, findsOneWidget); - return; - } - - /// Open the page that insides the settings page - Future openSettingsPage(SettingsPage page) async { - final button = find.byWidgetPredicate( - (widget) => widget is SettingsMenuElement && widget.page == page, - ); - - await scrollUntilVisible( - button, - 0, - scrollable: find.findSettingsMenuScrollable(), - ); - await pump(); - - expect(button, findsOneWidget); - await tapButton(button); - return; - } - - /// Restore the AppFlowy data storage location - Future restoreLocation() async { - final button = find.text(LocaleKeys.settings_common_reset.tr()); - expect(button, findsOneWidget); - await tapButton(button); - await pumpAndSettle(); - - final confirmButton = find.text(LocaleKeys.button_confirm.tr()); - expect(confirmButton, findsOneWidget); - await tapButton(confirmButton); - return; - } - - Future tapCustomLocationButton() async { - final button = find.byTooltip( - LocaleKeys.settings_files_changeLocationTooltips.tr(), - ); - expect(button, findsOneWidget); - await tapButton(button); - return; - } - - /// Enter user name - Future enterUserName(String name) async { - // Enable editing username - final editUsernameFinder = find.descendant( - of: find.byType(AccountUserProfile), - matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m), - ); - await tap(editUsernameFinder, warnIfMissed: false); - await pumpAndSettle(); - - final userNameFinder = find.descendant( - of: find.byType(AccountUserProfile), - matching: find.byType(FlowyTextField), - ); - await enterText(userNameFinder, name); - await pumpAndSettle(); - - await tap(find.text(LocaleKeys.button_save.tr())); - await pumpAndSettle(); - } - - // go to settings page and toggle enable RTL toolbar items - Future toggleEnableRTLToolbarItems() async { - await openSettings(); - await openSettingsPage(SettingsPage.workspace); - - final scrollable = find.findSettingsScrollable(); - await scrollUntilVisible( - find.byType(EnableRTLItemsSwitcher), - 0, - scrollable: scrollable, - ); - - final switcher = find.descendant( - of: find.byType(EnableRTLItemsSwitcher), - matching: find.byType(Toggle), - ); - - await tap(switcher); - - // tap anywhere to close the settings page - 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 deleted file mode 100644 index 5073425cad..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/util.dart +++ /dev/null @@ -1,9 +0,0 @@ -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'; diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart deleted file mode 100644 index 1b2f22b944..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/workspace.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'util.dart'; - -extension AppFlowyWorkspace on WidgetTester { - /// Open workspace menu - Future openWorkspaceMenu() async { - final workspaceWrapper = find.byType(SidebarSwitchWorkspaceButton); - expect(workspaceWrapper, findsOneWidget); - await tapButton(workspaceWrapper); - final workspaceMenu = find.byType(WorkspacesMenu); - expect(workspaceMenu, findsOneWidget); - } - - /// Open a workspace - Future openWorkspace(String name) async { - final workspace = find.descendant( - of: find.byType(WorkspaceMenuItem), - matching: find.findTextInFlowyText(name), - ); - expect(workspace, findsOneWidget); - await tapButton(workspace); - } - - Future changeWorkspaceName(String name) async { - final moreButton = find.descendant( - of: find.byType(WorkspaceMenuItem), - matching: find.byType(WorkspaceMoreActionList), - ); - expect(moreButton, findsOneWidget); - await hoverOnWidget( - moreButton, - onHover: () async { - await tapButton(moreButton); - // wait for the menu to open - final renameButton = 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); - await tapButton(find.text(LocaleKeys.button_ok.tr())); - }, - ); - } - - Future changeWorkspaceIcon(String icon) async { - final iconButton = find.descendant( - of: find.byType(WorkspaceMenuItem), - matching: find.byType(WorkspaceIcon), - ); - expect(iconButton, findsOneWidget); - await tapButton(iconButton); - final iconPicker = find.byType(FlowyIconEmojiPicker); - expect(iconPicker, findsOneWidget); - await tapButton(find.findTextInFlowyText(icon)); - } -} diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart new file mode 100644 index 0000000000..177bc7142b --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart @@ -0,0 +1,117 @@ +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/mock/mock_file_picker.dart'; +import 'util/util.dart'; +import 'package:path/path.dart' as p; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('customize the folder path', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('switch to B from A, then switch to A again', (tester) async { + final userA = uuid(); + final userB = uuid(); + + await TestFolder.cleanTestLocation(userA); + await TestFolder.cleanTestLocation(userB); + await TestFolder.setTestLocation(p.join(userA, appFlowyDataFolder)); + + await tester.initializeAppFlowy(); + + await tester.tapGoButton(); + tester.expectToSeeHomePage(); + + // switch to user B + { + // set user name to userA + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + await tester.enterUserName(userA); + + await tester.openSettingsPage(SettingsPage.files); + await tester.pumpAndSettle(); + + // mock the file_picker result + await mockGetDirectoryPath(userB); + await tester.tapCustomLocationButton(); + await tester.pumpAndSettle(); + tester.expectToSeeHomePage(); + + // set user name to userB + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + await tester.enterUserName(userB); + } + + // switch to the userA + { + await tester.openSettingsPage(SettingsPage.files); + await tester.pumpAndSettle(); + + // mock the file_picker result + await mockGetDirectoryPath(userA); + await tester.tapCustomLocationButton(); + + await tester.pumpAndSettle(); + tester.expectToSeeHomePage(); + tester.expectToSeeUserName(userA); + } + + // switch to the userB again + { + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.files); + await tester.pumpAndSettle(); + + // mock the file_picker result + await mockGetDirectoryPath(userB); + await tester.tapCustomLocationButton(); + + await tester.pumpAndSettle(); + tester.expectToSeeHomePage(); + tester.expectToSeeUserName(userB); + } + + await TestFolder.cleanTestLocation(userA); + await TestFolder.cleanTestLocation(userB); + }); + + testWidgets('reset to default location', (tester) async { + await tester.initializeAppFlowy(); + + await tester.tapGoButton(); + + // home and readme document + tester.expectToSeeHomePage(); + + // open settings and restore the location + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.files); + await tester.restoreLocation(); + + expect( + await TestFolder.defaultDevelopmentLocation(), + await TestFolder.currentLocation(), + ); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart new file mode 100644 index 0000000000..231f27d66e --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -0,0 +1,142 @@ +import 'dart:io'; + +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/entry_point.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/prelude.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_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class TestFolder { + /// Location / Path + + /// Set a given AppFlowy data storage location under test environment. + /// + /// To pass null means clear the location. + /// + /// The file_picker is a system component and can't be tapped, so using logic instead of tapping. + /// + static Future setTestLocation(String? name) async { + final location = await testLocation(name); + SharedPreferences.setMockInitialValues({ + KVKeys.pathLocation: location.path, + }); + return; + } + + /// Clean the location. + static Future cleanTestLocation(String? name) async { + final dir = await testLocation(name); + await dir.delete(recursive: true); + return; + } + + /// Get current using location. + static Future currentLocation() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(KVKeys.pathLocation)!; + } + + /// Get default location under development environment. + static Future defaultDevelopmentLocation() async { + final dir = await appFlowyApplicationDataDirectory(); + return dir.path; + } + + /// Get default location under test environment. + static Future testLocation(String? name) async { + final dir = await getApplicationDocumentsDirectory(); + var path = '${dir.path}/flowy_test'; + if (name != null) { + path += '/$name'; + } + return Directory(path).create(recursive: true); + } +} + +extension AppFlowyTestBase on WidgetTester { + Future initializeAppFlowy() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel('hotkey_manager'), + (MethodCall methodCall) async { + if (methodCall.method == 'unregisterAll') { + // do nothing + } + + return; + }); + + WidgetsFlutterBinding.ensureInitialized(); + await FlowyRunner.run(FlowyApp(), IntegrationMode.integrationTest); + + await wait(3000); + await pumpAndSettle(const Duration(seconds: 2)); + } + + Future tapButton( + Finder finder, { + int? pointer, + int buttons = kPrimaryButton, + bool warnIfMissed = true, + int milliseconds = 500, + }) async { + await tap( + finder, + buttons: buttons, + warnIfMissed: warnIfMissed, + ); + await pumpAndSettle(Duration(milliseconds: milliseconds)); + return; + } + + Future tapButtonWithName( + String tr, { + int milliseconds = 500, + }) async { + Finder button = find.text( + tr, + findRichText: true, + skipOffstage: false, + ); + if (button.evaluate().isEmpty) { + button = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == tr, + ); + } + await tapButton( + button, + milliseconds: milliseconds, + ); + return; + } + + Future tapButtonWithTooltip( + String tr, { + int milliseconds = 500, + }) async { + final button = find.byTooltip(tr); + await tapButton( + button, + milliseconds: milliseconds, + ); + return; + } + + Future wait(int milliseconds) async { + await pumpAndSettle(Duration(milliseconds: milliseconds)); + return; + } +} + +extension AppFlowyFinderTestBase on CommonFinders { + Finder findTextInFlowyText(String text) { + return find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == text, + ); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart new file mode 100644 index 0000000000..1523f7467a --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -0,0 +1,314 @@ +import 'dart:ui'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; +import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'util.dart'; + +extension CommonOperations on WidgetTester { + /// Get current file location of AppFlowy. + Future currentFileLocation() async { + return TestFolder.currentLocation(); + } + + /// Tap the GetStart button on the launch page. + Future tapGoButton() async { + final goButton = find.byType(GoButton); + await tapButton(goButton); + } + + /// Tap the + button on the home page. + Future tapAddButton() async { + final addButton = find.byType(AddButton); + await tapButton(addButton); + } + + /// Tap the create document button. + /// + /// Must call [tapAddButton] first. + Future tapCreateDocumentButton() async { + await tapButtonWithName(LocaleKeys.document_menuName.tr()); + } + + /// Tap the create grid button. + /// + /// Must call [tapAddButton] first. + Future tapCreateGridButton() async { + await tapButtonWithName(LocaleKeys.grid_menuName.tr()); + } + + /// Tap the create grid button. + /// + /// Must call [tapAddButton] first. + Future tapCreateCalendarButton() async { + await tapButtonWithName(LocaleKeys.calendar_menuName.tr()); + } + + /// Tap the import button. + /// + /// Must call [tapAddButton] first. + Future tapImportButton() async { + await tapButtonWithName(LocaleKeys.moreAction_import.tr()); + } + + /// Tap the import from text & markdown button. + /// + /// Must call [tapImportButton] first. + Future tapTextAndMarkdownButton() async { + await tapButtonWithName(LocaleKeys.importPanel_textAndMarkdown.tr()); + } + + /// Tap the LanguageSelectorOnWelcomePage widget on the launch page. + Future tapLanguageSelectorOnWelcomePage() async { + final languageSelector = find.byType(LanguageSelectorOnWelcomePage); + await tapButton(languageSelector); + } + + /// Tap languageItem on LanguageItemsListView. + /// + /// [scrollDelta] is the distance to scroll the ListView. + /// Default value is 100 + /// + /// If it is positive -> scroll down. + /// + /// If it is negative -> scroll up. + Future tapLanguageItem({ + required String languageCode, + String? countryCode, + double? scrollDelta, + }) async { + final languageItemsListView = find.descendant( + of: find.byType(ListView), + matching: find.byType(Scrollable), + ); + + final languageItem = find.byWidgetPredicate( + (widget) => + widget is LanguageItem && + widget.locale.languageCode == languageCode && + widget.locale.countryCode == countryCode, + ); + + // scroll the ListView until zHCNLanguageItem shows on the screen. + await scrollUntilVisible( + languageItem, + scrollDelta ?? 100, + scrollable: languageItemsListView, + // maxHeight of LanguageItemsListView + maxScrolls: 400, + ); + + try { + await tapButton(languageItem); + } on FlutterError catch (e) { + Log.warn('tapLanguageItem error: $e'); + } + } + + /// Hover on the widget. + Future hoverOnWidget( + Finder finder, { + Offset? offset, + Future Function()? onHover, + }) async { + try { + final gesture = await createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + await pump(); + await gesture.moveTo(offset ?? getCenter(finder)); + await pumpAndSettle(); + await onHover?.call(); + await gesture.removePointer(); + } catch (err) { + Log.error('hoverOnWidget error: $err'); + } + } + + /// Hover on the page name. + Future hoverOnPageName( + String name, { + Future Function()? onHover, + bool useLast = true, + }) async { + if (useLast) { + await hoverOnWidget(findPageName(name).last, onHover: onHover); + } else { + await hoverOnWidget(findPageName(name).first, onHover: onHover); + } + } + + /// open the page with given name. + Future openPage(String name) async { + final finder = findPageName(name); + expect(finder, findsOneWidget); + await tapButton(finder); + } + + /// Tap the ... button beside the page name. + /// + /// Must call [hoverOnPageName] first. + Future tapPageOptionButton() async { + final optionButton = find.byType(ViewDisclosureButton); + await tapButton(optionButton); + } + + /// Tap the delete page button. + Future tapDeletePageButton() async { + await tapPageOptionButton(); + await tapButtonWithName(ViewDisclosureAction.delete.name); + } + + /// Tap the rename page button. + Future tapRenamePageButton() async { + await tapPageOptionButton(); + await tapButtonWithName(ViewDisclosureAction.rename.name); + } + + /// Rename the page. + Future renamePage(String name) async { + await tapRenamePageButton(); + await enterText(find.byType(TextFormField), name); + await tapOKButton(); + } + + Future tapOKButton() async { + final okButton = find.byWidgetPredicate( + (widget) => + widget is PrimaryTextButton && + widget.label == LocaleKeys.button_OK.tr(), + ); + await tapButton(okButton); + } + + /// Tap the restore button. + /// + /// the restore button will show after the current page is deleted. + Future tapRestoreButton() async { + final restoreButton = find.textContaining( + LocaleKeys.deletePagePrompt_restore.tr(), + ); + await tapButton(restoreButton); + } + + /// Tap the delete permanently button. + /// + /// the restore button will show after the current page is deleted. + Future tapDeletePermanentlyButton() async { + final restoreButton = find.textContaining( + LocaleKeys.deletePagePrompt_deletePermanent.tr(), + ); + await tapButton(restoreButton); + } + + /// Tap the share button above the document page. + Future tapShareButton() async { + final shareButton = find.byWidgetPredicate( + (widget) => widget is DocumentShareButton, + ); + await tapButton(shareButton); + } + + /// Tap the export markdown button + /// + /// Must call [tapShareButton] first. + Future tapMarkdownButton() async { + final markdownButton = find.textContaining( + LocaleKeys.shareAction_markdown.tr(), + ); + await tapButton(markdownButton); + } + + Future createNewPageWithName(ViewLayoutPB layout, String name) async { + // create a new page + await tapAddButton(); + await tapButtonWithName(layout.menuName); + await pumpAndSettle(); + + // hover on it and change it's name + await hoverOnPageName( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + onHover: () async { + await renamePage(name); + await pumpAndSettle(); + }, + ); + await pumpAndSettle(); + } + + Future simulateKeyEvent( + LogicalKeyboardKey key, { + bool isControlPressed = false, + bool isShiftPressed = false, + bool isAltPressed = false, + bool isMetaPressed = false, + }) async { + if (isControlPressed) { + await simulateKeyDownEvent(LogicalKeyboardKey.control); + } + if (isShiftPressed) { + await simulateKeyDownEvent(LogicalKeyboardKey.shift); + } + if (isAltPressed) { + await simulateKeyDownEvent(LogicalKeyboardKey.alt); + } + if (isMetaPressed) { + await simulateKeyDownEvent(LogicalKeyboardKey.meta); + } + await simulateKeyDownEvent(key); + await simulateKeyUpEvent(key); + if (isControlPressed) { + await simulateKeyUpEvent(LogicalKeyboardKey.control); + } + if (isShiftPressed) { + await simulateKeyUpEvent(LogicalKeyboardKey.shift); + } + if (isAltPressed) { + await simulateKeyUpEvent(LogicalKeyboardKey.alt); + } + if (isMetaPressed) { + await simulateKeyUpEvent(LogicalKeyboardKey.meta); + } + await pumpAndSettle(); + } +} + +extension ViewLayoutPBTest on ViewLayoutPB { + String get menuName { + switch (this) { + case ViewLayoutPB.Grid: + return LocaleKeys.grid_menuName.tr(); + case ViewLayoutPB.Board: + return LocaleKeys.board_menuName.tr(); + case ViewLayoutPB.Document: + return LocaleKeys.document_menuName.tr(); + case ViewLayoutPB.Calendar: + return LocaleKeys.calendar_menuName.tr(); + default: + throw UnsupportedError('Unsupported layout: $this'); + } + } + + String get referencedMenuName { + switch (this) { + case ViewLayoutPB.Grid: + return LocaleKeys.document_plugins_referencedGrid.tr(); + case ViewLayoutPB.Board: + return LocaleKeys.document_plugins_referencedBoard.tr(); + case ViewLayoutPB.Calendar: + return LocaleKeys.document_plugins_referencedCalendar.tr(); + default: + throw UnsupportedError('Unsupported layout: $this'); + } + } +} diff --git a/frontend/appflowy_flutter/integration_test/shared/data.dart b/frontend/appflowy_flutter/integration_test/util/data.dart similarity index 94% rename from frontend/appflowy_flutter/integration_test/shared/data.dart rename to frontend/appflowy_flutter/integration_test/util/data.dart index c1777638d3..5532904aca 100644 --- a/frontend/appflowy_flutter/integration_test/shared/data.dart +++ b/frontend/appflowy_flutter/integration_test/util/data.dart @@ -51,7 +51,11 @@ class TestWorkspaceService { Future setUpAll() async { final root = await workspace.root; final path = root.path; - SharedPreferences.setMockInitialValues({KVKeys.pathLocation: path}); + SharedPreferences.setMockInitialValues( + { + KVKeys.pathLocation: path, + }, + ); } /// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control. @@ -59,7 +63,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/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart new file mode 100644 index 0000000000..63e14b591d --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -0,0 +1,1016 @@ +import 'dart:io'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/create_filter_list.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/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/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:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import 'base.dart'; +import 'common_operations.dart'; +import 'expectation.dart'; +import 'package:path/path.dart' as p; + +import 'mock/mock_file_picker.dart'; + +extension AppFlowyDatabaseTest on WidgetTester { + Future openV020database() async { + await initializeAppFlowy(); + await tapGoButton(); + + // expect to see a readme page + expectToSeePageName(readme); + + await tapAddButton(); + await tapImportButton(); + + final testFileNames = ['v020.afdb']; + final fileLocation = await currentFileLocation(); + for (final fileName in testFileNames) { + final str = await rootBundle.loadString( + p.join( + 'assets/test/workspaces/database', + fileName, + ), + ); + File(p.join(fileLocation, fileName)).writeAsStringSync(str); + } + // mock get files + await mockPickFilePaths(testFileNames, name: 'import_files'); + await tapDatabaseRawDataButton(); + await openPage('v020'); + } + + Future hoverOnFirstRowOfGrid() async { + final findRow = find.byType(GridRow); + expect(findRow, findsWidgets); + + final firstRow = findRow.first; + await hoverOnWidget(firstRow); + } + + Future editCell({ + required int rowIndex, + required FieldType fieldType, + required String input, + }) async { + final cell = cellFinder(rowIndex, fieldType); + + expect(cell, findsOneWidget); + await enterText(cell, input); + await pumpAndSettle(); + } + + Finder cellFinder(int rowIndex, FieldType fieldType) { + final findRow = find.byType(GridRow, skipOffstage: false); + final findCell = finderForFieldType(fieldType); + return find.descendant( + of: findRow.at(rowIndex), + matching: findCell, + skipOffstage: false, + ); + } + + Future tapCheckboxCellInGrid({ + required int rowIndex, + }) async { + final cell = cellFinder(rowIndex, FieldType.Checkbox); + + final button = find.descendant( + of: cell, + matching: find.byType(FlowyIconButton), + ); + + expect(cell, findsOneWidget); + await tapButton(button); + } + + Future assertCheckboxCell({ + required int rowIndex, + required bool isSelected, + }) async { + final cell = cellFinder(rowIndex, FieldType.Checkbox); + var finder = find.byType(CheckboxCellUncheck); + if (isSelected) { + finder = find.byType(CheckboxCellCheck); + } + + expect( + find.descendant( + of: cell, + matching: finder, + ), + findsOneWidget, + ); + } + + Future tapCellInGrid({ + required int rowIndex, + required FieldType fieldType, + }) async { + final cell = cellFinder(rowIndex, fieldType); + expect(cell, findsOneWidget); + await tapButton(cell, warnIfMissed: false); + } + + /// The [fieldName] must be uqniue in the grid. + Future assertCellContent({ + required int rowIndex, + required FieldType fieldType, + required String content, + }) async { + final findCell = cellFinder(rowIndex, fieldType); + final findContent = find.descendant( + of: findCell, + matching: find.text(content), + skipOffstage: false, + ); + + final text = find.descendant( + of: find.byType(TextField), + matching: findContent, + skipOffstage: false, + ); + + expect(text, findsOneWidget); + } + + Future assertSingleSelectOption({ + required int rowIndex, + required String content, + }) async { + final findCell = cellFinder(rowIndex, FieldType.SingleSelect); + if (content.isNotEmpty) { + final finder = find.descendant( + of: findCell, + matching: find.byWidgetPredicate( + (widget) => widget is SelectOptionTag && widget.name == content, + ), + ); + expect(finder, findsOneWidget); + } + } + + Future assertMultiSelectOption({ + required int rowIndex, + required List contents, + }) async { + final findCell = cellFinder(rowIndex, FieldType.MultiSelect); + for (final content in contents) { + if (content.isNotEmpty) { + final finder = find.descendant( + of: findCell, + matching: find.byWidgetPredicate( + (widget) => widget is SelectOptionTag && widget.name == content, + ), + ); + expect(finder, findsOneWidget); + } + } + } + + Future assertChecklistCellInGrid({ + required int rowIndex, + required double percent, + }) async { + final findCell = cellFinder(rowIndex, FieldType.Checklist); + final finder = find.descendant( + of: findCell, + matching: find.byWidgetPredicate( + (widget) { + if (widget is ChecklistProgressBar) { + return widget.percent == percent; + } + return false; + }, + ), + ); + expect(finder, findsOneWidget); + } + + Future assertDateCellInGrid({ + required int rowIndex, + required FieldType fieldType, + required String content, + }) async { + final findRow = find.byType(GridRow, skipOffstage: false); + final findCell = find.descendant( + of: findRow.at(rowIndex), + matching: find.byWidgetPredicate( + (widget) => widget is GridDateCell && widget.fieldType == fieldType, + ), + skipOffstage: false, + ); + + final dateCellText = find.descendant( + of: findCell, + matching: find.byType(GridDateCellText), + ); + + final text = find.descendant( + of: dateCellText, + matching: find.byWidgetPredicate( + (widget) { + if (widget is FlowyText) { + return widget.text == content; + } + return false; + }, + ), + skipOffstage: false, + ); + expect(text, findsOneWidget); + } + + Future selectDay({ + required int content, + }) async { + final findCalendar = find.byType(TableCalendar); + final findDay = find.text(content.toString()); + + final finder = find.descendant( + of: findCalendar, + matching: findDay, + ); + + await tapButton(finder); + } + + Future tapSelectOptionCellInGrid({ + required int rowIndex, + required FieldType fieldType, + }) async { + assert( + fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect, + ); + + final findRow = find.byType(GridRow); + final findCell = finderForFieldType(fieldType); + + final cell = find.descendant( + of: findRow.at(rowIndex), + matching: findCell, + ); + + await tapButton(cell); + } + + /// The [SelectOptionCellEditor] must be opened first. + Future createOption({ + required String name, + }) async { + final findEditor = find.byType(SelectOptionCellEditor); + expect(findEditor, findsOneWidget); + + final findTextField = find.byType(SelectOptionTextField); + expect(findTextField, findsOneWidget); + + await enterText(findTextField, name); + await pump(); + + await testTextInput.receiveAction(TextInputAction.done); + await pumpAndSettle(); + } + + Future findSelectOptionWithNameInGrid({ + required int rowIndex, + required String name, + }) async { + final findRow = find.byType(GridRow); + final option = find.byWidgetPredicate( + (widget) => widget is SelectOptionTag && widget.name == name, + ); + + final cell = find.descendant( + of: findRow.at(rowIndex), + matching: option, + ); + + expect(cell, findsOneWidget); + } + + Future openFirstRowDetailPage() async { + await hoverOnFirstRowOfGrid(); + + final expandButton = find.byType(PrimaryCellAccessory); + expect(expandButton, findsOneWidget); + await tapButton(expandButton); + } + + Future hoverRowBanner() async { + final banner = find.byType(RowBanner); + expect(banner, findsOneWidget); + + await startGesture( + getTopLeft(banner), + kind: PointerDeviceKind.mouse, + ); + + await pumpAndSettle(); + } + + Future openEmojiPicker() async { + await tapButton(find.byType(EmojiPickerButton)); + await tapButton(find.byType(EmojiSelectionMenu)); + } + + /// Must call [openEmojiPicker] first + Future switchToEmojiList() async { + final icon = find.byIcon(Icons.tag_faces); + await tapButton(icon); + } + + Future tapEmoji(String emoji) async { + final emojiWidget = find.text(emoji); + await tapButton(emojiWidget); + } + + Future scrollGridByOffset(Offset offset) async { + await drag(find.byType(GridPage), offset); + await pumpAndSettle(); + } + + Future scrollRowDetailByOffset(Offset offset) async { + await drag(find.byType(RowDetailPage), offset); + await pumpAndSettle(); + } + + Future scrollToRight(Finder find) async { + final size = getSize(find); + await drag(find, Offset(-size.width, 0)); + await pumpAndSettle(const Duration(milliseconds: 500)); + } + + Future tapNewPropertyButton() async { + await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr()); + await pumpAndSettle(); + } + + Future tapGridFieldWithName(String name) async { + final field = find.byWidgetPredicate( + (widget) => widget is FieldCellButton && widget.field.name == name, + ); + await tapButton(field); + await pumpAndSettle(); + } + + /// Should call [tapGridFieldWithName] first. + Future tapEditPropertyButton() async { + await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr()); + await pumpAndSettle(const Duration(milliseconds: 200)); + } + + /// Should call [tapGridFieldWithName] first. + Future tapDeletePropertyButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldActionCell && widget.action == FieldAction.delete, + ); + await tapButton(field); + } + + /// Should call [tapGridFieldWithName] first. + Future tapDialogOkButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is PrimaryTextButton && + widget.label == LocaleKeys.button_OK.tr(), + ); + await tapButton(field); + } + + /// Should call [tapGridFieldWithName] first. + Future tapDuplicatePropertyButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldActionCell && widget.action == FieldAction.duplicate, + ); + await tapButton(field); + } + + /// Should call [tapGridFieldWithName] first. + Future tapHidePropertyButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldActionCell && widget.action == FieldAction.hide, + ); + await tapButton(field); + } + + Future tapRowDetailPageCreatePropertyButton() async { + await tapButton(find.byType(CreateRowFieldButton)); + } + + Future tapRowDetailPageDeleteRowButton() async { + await tapButton(find.byType(RowDetailPageDeleteButton)); + } + + Future tapRowDetailPageDuplicateRowButton() async { + await tapButton(find.byType(RowDetailPageDuplicateButton)); + } + + Future tapTypeOptionButton() async { + await tapButton(find.byType(SwitchFieldButton)); + } + + Future tapEscButton() async { + await sendKeyEvent(LogicalKeyboardKey.escape); + } + + /// Must call [tapTypeOptionButton] first. + Future selectFieldType(FieldType fieldType) async { + final fieldTypeCell = find.byType(FieldTypeCell); + final fieldTypeButton = find.descendant( + of: fieldTypeCell, + matching: find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == fieldType.title(), + ), + ); + await tapButton(fieldTypeButton); + } + + /// Each field has its own cell, so we can find the corresponding cell by + /// the field type after create a new field. + Future findCellByFieldType(FieldType fieldType) async { + final finder = finderForFieldType(fieldType); + expect(finder, findsWidgets); + } + + Future assertNumberOfFieldsInGridPage(int num) async { + expect(find.byType(GridFieldCell), findsNWidgets(num)); + } + + Future assertNumberOfRowsInGridPage(int num) async { + expect( + find.byType(GridRow, skipOffstage: false), + findsNWidgets(num), + ); + } + + Future assertDocumentExistInRowDetailPage() async { + expect(find.byType(RowDocument), findsOneWidget); + } + + /// Check the field type of the [FieldCellButton] is the same as the name. + Future assertFieldTypeWithFieldName( + String name, + FieldType fieldType, + ) async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldCellButton && + widget.field.fieldType == fieldType && + widget.field.name == name, + ); + + expect(field, findsOneWidget); + } + + Future findFieldWithName(String name) async { + final field = find.byWidgetPredicate( + (widget) => widget is FieldCellButton && widget.field.name == name, + ); + expect(field, findsOneWidget); + } + + Future noFieldWithName(String name) async { + final field = find.byWidgetPredicate( + (widget) => widget is FieldCellButton && widget.field.name == name, + ); + expect(field, findsNothing); + } + + Future renameField(String newName) async { + final textField = find.byType(FieldNameTextField); + expect(textField, findsOneWidget); + await enterText(textField, newName); + await pumpAndSettle(); + } + + Future dismissFieldEditor() async { + await sendKeyEvent(LogicalKeyboardKey.escape); + await pumpAndSettle(const Duration(milliseconds: 200)); + } + + Future findFieldEditor(dynamic matcher) async { + final finder = find.byType(FieldEditor); + expect(finder, matcher); + } + + Future findDateEditor(dynamic matcher) async { + final finder = find.byType(DateCellEditor); + expect(finder, matcher); + } + + Future findSelectOptionEditor(dynamic matcher) async { + final finder = find.byType(SelectOptionCellEditor); + expect(finder, matcher); + } + + Future dismissSelectOptionEditor() async { + await sendKeyEvent(LogicalKeyboardKey.escape); + await pumpAndSettle(); + } + + Future tapCreateRowButtonInGrid() async { + await tapButton(find.byType(GridAddRowButton)); + } + + Future tapCreateRowButtonInRowMenuOfGrid() async { + await tapButton(find.byType(InsertRowButton)); + } + + Future tapRowMenuButtonInGrid() async { + await tapButton(find.byType(RowMenuButton)); + } + + /// Should call [tapRowMenuButtonInGrid] first. + Future tapDeleteOnRowMenu() async { + await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); + } + + Future assertRowCountInGridPage(int num) async { + final text = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == rowCountString(num), + ); + expect(text, findsOneWidget); + } + + Future createField(FieldType fieldType, String name) async { + await scrollToRight(find.byType(GridPage)); + await tapNewPropertyButton(); + await renameField(name); + await tapTypeOptionButton(); + await selectFieldType(fieldType); + await dismissFieldEditor(); + } + + Future tapDatabaseSettingButton() async { + await tapButton(find.byType(SettingButton)); + } + + Future tapDatabaseFilterButton() async { + await tapButton(find.byType(FilterButton)); + } + + Future tapDatabaseSortButton() async { + await tapButton(find.byType(SortButton)); + } + + Future tapCreateFilterByFieldType( + FieldType fieldType, + String title, + ) async { + final findFilter = find.byWidgetPredicate( + (widget) => + widget is GridFilterPropertyCell && + widget.fieldInfo.fieldType == fieldType && + widget.fieldInfo.name == title, + ); + + await tapButton(findFilter); + } + + Future tapFilterButtonInGrid(String filterName) async { + final findFilter = find.byType(FilterMenuItem); + final button = find.descendant( + of: findFilter, + matching: find.text(filterName), + ); + + await tapButton(button); + } + + Future tapCreateSortByFieldType( + FieldType fieldType, + String title, + ) async { + final findSort = find.byWidgetPredicate( + (widget) => + widget is GridSortPropertyCell && + widget.fieldInfo.fieldType == fieldType && + widget.fieldInfo.name == title, + ); + + await tapButton(findSort); + } + + // Must call [tapSortMenuInSettingBar] first. + Future tapCreateSortByFieldTypeInSortMenu( + FieldType fieldType, + String title, + ) async { + await tapButton(find.byType(DatabaseAddSortButton)); + + final findSort = find.byWidgetPredicate( + (widget) => + widget is GridSortPropertyCell && + widget.fieldInfo.fieldType == fieldType && + widget.fieldInfo.name == title, + ); + + await tapButton(findSort); + await pumpAndSettle(); + } + + Future tapSortMenuInSettingBar() async { + await tapButton(find.byType(SortMenu)); + await pumpAndSettle(); + } + + /// Must call [tapSortMenuInSettingBar] first. + Future tapSortButtonByName(String name) async { + final findSortItem = find.byWidgetPredicate( + (widget) => + widget is DatabaseSortItem && widget.sortInfo.fieldInfo.name == name, + ); + await tapButton(findSortItem); + } + + /// Must call [tapSortButtonByName] first. + Future tapSortByDescending() async { + await tapButton( + find.descendant( + of: find.byType(OrderPannelItem), + matching: find.byWidgetPredicate( + (widget) => + widget is FlowyText && + widget.text == LocaleKeys.grid_sort_descending.tr(), + ), + ), + ); + await sendKeyEvent(LogicalKeyboardKey.escape); + await pumpAndSettle(); + } + + /// Must call [tapSortMenuInSettingBar] first. + Future tapAllSortButton() async { + await tapButton(find.byType(DatabaseDeleteSortButton)); + } + + Future scrollOptionFilterListByOffset(Offset offset) async { + await drag(find.byType(SelectOptionFilterEditor), offset); + await pumpAndSettle(); + } + + Future enterTextInTextFilter(String text) async { + final findEditor = find.byType(TextFilterEditor); + final findTextField = find.descendant( + of: findEditor, + matching: find.byType(FlowyTextField), + ); + + await enterText(findTextField, text); + await pumpAndSettle(const Duration(milliseconds: 300)); + } + + Future tapDisclosureButtonInFinder(Finder finder) async { + final findDisclosure = find.descendant( + of: finder, + matching: find.byType(DisclosureButton), + ); + + await tapButton(findDisclosure); + } + + /// must call [tapDisclosureButtonInFinder] first. + Future tapDeleteFilterButtonInGrid() async { + await tapButton(find.text(LocaleKeys.grid_settings_deleteFilter.tr())); + } + + Future tapCheckboxFilterButtonInGrid() async { + await tapButton(find.byType(CheckboxFilterConditionList)); + } + + Future tapChecklistFilterButtonInGrid() async { + await tapButton(find.byType(ChecklistFilterConditionList)); + } + + /// The [SelectOptionFilterList] must show up first. + Future tapOptionFilterWithName(String name) async { + final findCell = find.descendant( + of: find.byType(SelectOptionFilterList), + matching: find.byWidgetPredicate( + (widget) => + widget is SelectOptionFilterCell && widget.option.name == name, + skipOffstage: false, + ), + skipOffstage: false, + ); + expect(findCell, findsOneWidget); + await tapButton(findCell, warnIfMissed: false); + } + + Future tapCheckedButtonOnCheckboxFilter() async { + final button = find.descendant( + of: find.byType(HoverButton), + matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()), + ); + + await tapButton(button); + } + + Future tapUnCheckedButtonOnCheckboxFilter() async { + final button = find.descendant( + of: find.byType(HoverButton), + matching: find.text(LocaleKeys.grid_checkboxFilter_isUnchecked.tr()), + ); + + await tapButton(button); + } + + Future tapCompletedButtonOnChecklistFilter() async { + final button = find.descendant( + of: find.byType(HoverButton), + matching: find.text(LocaleKeys.grid_checklistFilter_isComplete.tr()), + ); + + await tapButton(button); + } + + Future tapUnCompletedButtonOnChecklistFilter() async { + final button = find.descendant( + of: find.byType(HoverButton), + matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()), + ); + + await tapButton(button); + } + + /// Should call [tapDatabaseSettingButton] first. + Future tapDatabaseLayoutButton() async { + final findSettingItem = find.byType(DatabaseSettingItem); + final findLayoutButton = find.byWidgetPredicate( + (widget) => + widget is FlowyText && + widget.text == DatabaseSettingAction.showLayout.title(), + ); + + final button = find.descendant( + of: findSettingItem, + matching: findLayoutButton, + ); + + await tapButton(button); + } + + Future tapCalendarLayoutSettingButton() async { + final findSettingItem = find.byType(DatabaseSettingItem); + final findLayoutButton = find.byWidgetPredicate( + (widget) => + widget is FlowyText && + widget.text == DatabaseSettingAction.showCalendarLayout.title(), + ); + + final button = find.descendant( + of: findSettingItem, + matching: findLayoutButton, + ); + + await tapButton(button); + } + + Future tapFirstDayOfWeek() async { + await tapButton(find.byType(FirstDayOfWeek)); + } + + Future tapFirstDayOfWeekStartFromSunday() async { + final finder = find.byWidgetPredicate( + (widget) => widget is StartFromButton && widget.dayIndex == 0, + ); + await tapButton(finder); + } + + Future tapFirstDayOfWeekStartFromMonday() async { + final finder = find.byWidgetPredicate( + (widget) => widget is StartFromButton && widget.dayIndex == 1, + ); + await tapButton(finder); + + // Dismiss the popover overlay in cause of obscure the tapButton + // in the next test case. + await sendKeyEvent(LogicalKeyboardKey.escape); + await pumpAndSettle(const Duration(milliseconds: 200)); + } + + void assertFirstDayOfWeekStartFromMonday() { + final finder = find.byWidgetPredicate( + (widget) => + widget is StartFromButton && + widget.dayIndex == 1 && + widget.isSelected == true, + ); + expect(finder, findsOneWidget); + } + + void assertFirstDayOfWeekStartFromSunday() { + final finder = find.byWidgetPredicate( + (widget) => + widget is StartFromButton && + widget.dayIndex == 0 && + widget.isSelected == true, + ); + expect(finder, findsOneWidget); + } + + Future tapCreateLinkedDatabaseViewButton(AddButtonAction action) async { + final findAddButton = find.byType(AddDatabaseViewButton); + await tapButton(findAddButton); + + final findCreateButton = find.byWidgetPredicate( + (widget) => + widget is TarBarAddButtonActionCell && widget.action == action, + ); + await tapButton(findCreateButton); + } + + Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) { + return find.byWidgetPredicate( + (widget) => widget is TabBarItemButton && widget.view.layout == layout, + ); + } + + Finder findTabBarLinkViewByViewName(String name) { + return find.byWidgetPredicate( + (widget) => widget is TabBarItemButton && widget.view.name == name, + ); + } + + Future renameLinkedView(Finder linkedView, String name) async { + await tap(linkedView, buttons: kSecondaryButton); + await pumpAndSettle(); + + await tapButton( + find.byWidgetPredicate( + (widget) => + widget is ActionCellWidget && + widget.action == TabBarViewAction.rename, + ), + ); + + await enterText( + find.descendant( + of: find.byType(FlowyFormTextInput), + matching: find.byType(TextFormField), + ), + name, + ); + + final field = find.byWidgetPredicate( + (widget) => + widget is PrimaryTextButton && + widget.label == LocaleKeys.button_OK.tr(), + ); + await tapButton(field); + } + + Future deleteDatebaseView(Finder linkedView) async { + await tap(linkedView, buttons: kSecondaryButton); + await pumpAndSettle(); + + await tapButton( + find.byWidgetPredicate( + (widget) => + widget is ActionCellWidget && + widget.action == TabBarViewAction.delete, + ), + ); + + final okButton = find.byWidgetPredicate( + (widget) => + widget is PrimaryTextButton && + widget.label == LocaleKeys.button_OK.tr(), + ); + await tapButton(okButton); + } + + Future assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) async { + switch (layout) { + case DatabaseLayoutPB.Board: + expect(find.byType(BoardPage), findsOneWidget); + break; + case DatabaseLayoutPB.Calendar: + expect(find.byType(CalendarPage), findsOneWidget); + break; + case DatabaseLayoutPB.Grid: + expect(find.byType(GridPage), findsOneWidget); + break; + default: + throw Exception('Unknown database layout type: $layout'); + } + } + + Future selectDatabaseLayoutType(DatabaseLayoutPB layout) async { + final findLayoutCell = find.byType(DatabaseViewLayoutCell); + final findText = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == layout.layoutName(), + ); + + final button = find.descendant( + of: findLayoutCell, + matching: findText, + ); + + await tapButton(button); + } + + Future assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async { + expect(finderForDatabaseLayoutType(layout), findsOneWidget); + } + + Future tapDatabaseRawDataButton() async { + await tapButtonWithName(LocaleKeys.importPanel_database.tr()); + } +} + +Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) { + switch (layout) { + case DatabaseLayoutPB.Board: + return find.byType(BoardPage); + case DatabaseLayoutPB.Calendar: + return find.byType(CalendarPage); + case DatabaseLayoutPB.Grid: + return find.byType(GridPage); + default: + throw Exception('Unknown database layout type: $layout'); + } +} + +Finder finderForFieldType(FieldType fieldType) { + switch (fieldType) { + case FieldType.Checkbox: + return find.byType(GridCheckboxCell, skipOffstage: false); + case FieldType.DateTime: + return find.byType(GridDateCell, skipOffstage: false); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return find.byType(GridDateCell, skipOffstage: false); + case FieldType.SingleSelect: + return find.byType(GridSingleSelectCell, skipOffstage: false); + case FieldType.MultiSelect: + return find.byType(GridMultiSelectCell, skipOffstage: false); + case FieldType.Checklist: + return find.byType(GridChecklistCell, skipOffstage: false); + case FieldType.Number: + return find.byType(GridNumberCell, skipOffstage: false); + case FieldType.RichText: + return find.byType(GridTextCell, skipOffstage: false); + case FieldType.URL: + return find.byType(GridURLCell, skipOffstage: false); + default: + throw Exception('Unknown field type: $fieldType'); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart new file mode 100644 index 0000000000..31187e1fbc --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -0,0 +1,51 @@ +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:flutter_test/flutter_test.dart'; + +import 'ime.dart'; +import 'util.dart'; + +extension EditorWidgetTester on WidgetTester { + EditorOperations get editor => EditorOperations(this); +} + +class EditorOperations { + const EditorOperations(this.tester); + + final WidgetTester tester; + + EditorState getCurrentEditorState() { + return tester + .widget(find.byType(AppFlowyEditor)) + .editorState; + } + + /// Tap the line of editor at [index] + Future tapLineOfEditorAt(int index) async { + final textBlocks = find.byType(TextBlockComponentWidget); + await tester.tapAt(tester.getTopRight(textBlocks.at(index))); + } + + /// Hover on cover plugin button above the document + Future hoverOnCoverPluginAddButton() async { + final editor = find.byWidgetPredicate( + (widget) => widget is AppFlowyEditor, + ); + await tester.hoverOnWidget( + editor, + offset: tester.getTopLeft(editor).translate(20, 20), + ); + } + + /// trigger the slash command (selection menu) + Future showSlashMenu() 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); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart new file mode 100644 index 0000000000..1f6c329d66 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -0,0 +1,88 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/banner.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const String readme = 'Read me'; + +extension Expectation on WidgetTester { + /// Expect to see the home page and with a default read me page. + void expectToSeeHomePage() { + expect(find.byType(HomeStack), findsOneWidget); + expect(find.textContaining(readme), findsWidgets); + } + + /// Expect to see the page name on the home page. + void expectToSeePageName(String name) { + final pageName = findPageName(name); + expect(pageName, findsOneWidget); + } + + /// Expect not to see the page name on the home page. + void expectNotToSeePageName(String name) { + final pageName = findPageName(name); + expect(pageName, findsNothing); + } + + /// Expect to see the document banner. + void expectToSeeDocumentBanner() { + expect(find.byType(DocumentBanner), findsOneWidget); + } + + /// Expect not to see the document banner. + void expectNotToSeeDocumentBanner() { + expect(find.byType(DocumentBanner), findsNothing); + } + + /// Expect to the markdown file export success dialog. + void expectToExportSuccess() { + final exportSuccess = find.byWidgetPredicate( + (widget) => + widget is FlowyText && + widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(), + ); + expect(exportSuccess, findsOneWidget); + } + + /// Expect to see the add button and icon button inside the document. + void expectToSeePluginAddCoverAndIconButton() { + final addCover = find.textContaining( + LocaleKeys.document_plugins_cover_addCover.tr(), + ); + final addIcon = find.textContaining( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ); + expect(addCover, findsOneWidget); + expect(addIcon, findsOneWidget); + } + + /// Expect to see the user name on the home page + void expectToSeeUserName(String name) { + final userName = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == name, + ); + expect(userName, findsOneWidget); + } + + /// Expect to see a text + void expectToSeeText(String text) { + Finder textWidget = find.textContaining(text, findRichText: true); + if (textWidget.evaluate().isEmpty) { + textWidget = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.text == text, + ); + } + expect(textWidget, findsOneWidget); + } + + /// Find the page name on the home page. + Finder findPageName(String name) { + return find.byWidgetPredicate( + (widget) => widget is ViewSectionItem && widget.view.name == name, + skipOffstage: false, + ); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/ime.dart b/frontend/appflowy_flutter/integration_test/util/ime.dart new file mode 100644 index 0000000000..30b1388e0d --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/ime.dart @@ -0,0 +1,54 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension IME on WidgetTester { + IMESimulator get ime => IMESimulator(this); +} + +class IMESimulator { + IMESimulator(this.tester) { + client = findDeltaTextInputClient(); + } + + final WidgetTester tester; + late final DeltaTextInputClient client; + + Future insertText(String text) async { + for (final c in text.characters) { + await insertCharacter(c); + } + } + + Future insertCharacter(String character) async { + final value = client.currentTextEditingValue; + if (value == null) { + assert(false); + return; + } + final deltas = [ + TextEditingDeltaInsertion( + textInserted: character, + oldText: value.text.replaceRange( + value.selection.start, + value.selection.end, + '', + ), + insertionOffset: value.selection.baseOffset, + selection: TextSelection.collapsed( + offset: value.selection.baseOffset + 1, + ), + composing: TextRange.empty, + ), + ]; + client.updateEditingValueWithDeltas(deltas); + await tester.pumpAndSettle(); + } + + DeltaTextInputClient findDeltaTextInputClient() { + final finder = find.byType(KeyboardServiceWidget); + final KeyboardServiceWidgetState state = tester.state(finder); + return state.textInputService as DeltaTextInputClient; + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/keyboard.dart b/frontend/appflowy_flutter/integration_test/util/keyboard.dart new file mode 100644 index 0000000000..d792b92c66 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/keyboard.dart @@ -0,0 +1,14 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart' as flutter_test; + +class FlowyTestKeyboard { + static Future simulateKeyDownEvent( + List keys, { + required flutter_test.WidgetTester tester, + }) async { + for (final LogicalKeyboardKey key in keys) { + await flutter_test.simulateKeyDownEvent(key); + await tester.pumpAndSettle(); + } + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart b/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart new file mode 100644 index 0000000000..390d77d194 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart @@ -0,0 +1,101 @@ +import 'dart:io'; + +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:file_picker/file_picker.dart' as fp; +import 'package:path/path.dart' as p; +import '../util.dart'; + +class MockFilePicker implements FilePickerService { + MockFilePicker({ + this.mockPath = '', + this.mockPaths = const [], + }); + + final String mockPath; + final List mockPaths; + + @override + Future getDirectoryPath({String? title}) { + return Future.value(mockPath); + } + + @override + Future saveFile({ + String? dialogTitle, + String? fileName, + String? initialDirectory, + fp.FileType type = fp.FileType.any, + List? allowedExtensions, + bool lockParentWindow = false, + }) { + return Future.value(mockPath); + } + + @override + Future pickFiles({ + String? dialogTitle, + String? initialDirectory, + fp.FileType type = fp.FileType.any, + List? allowedExtensions, + Function(fp.FilePickerStatus p1)? onFileLoading, + bool allowCompression = true, + bool allowMultiple = false, + bool withData = false, + bool withReadStream = false, + bool lockParentWindow = false, + }) { + final platformFiles = mockPaths + .map((e) => fp.PlatformFile(path: e, name: '', size: 0)) + .toList(); + return Future.value( + FilePickerResult( + platformFiles, + ), + ); + } +} + +Future mockGetDirectoryPath(String? name) async { + final dir = await TestFolder.testLocation(name); + getIt.unregister(); + getIt.registerFactory( + () => MockFilePicker( + mockPath: dir.path, + ), + ); + return; +} + +Future mockSaveFilePath(String? name, String fileName) async { + final dir = await TestFolder.testLocation(name); + final path = p.join(dir.path, fileName); + getIt.unregister(); + getIt.registerFactory( + () => MockFilePicker( + mockPath: path, + ), + ); + return path; +} + +Future> mockPickFilePaths( + List fileNames, { + String? name, + String? customPath, +}) async { + late final Directory dir; + if (customPath != null) { + dir = Directory(customPath); + } else { + dir = await TestFolder.testLocation(name); + } + final paths = fileNames.map((e) => p.join(dir.path, e)).toList(); + getIt.unregister(); + getIt.registerFactory( + () => MockFilePicker( + mockPaths: paths, + ), + ); + return paths; +} diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart new file mode 100644 index 0000000000..ec2e3149d9 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart @@ -0,0 +1,76 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:mocktail/mocktail.dart'; +import 'dart:convert'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; +import 'package:http/http.dart' as http; +import 'dart:async'; + +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); + + var 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(); + } + } + } + return; + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/settings.dart b/frontend/appflowy_flutter/integration_test/util/settings.dart new file mode 100644 index 0000000000..4f350522cb --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/settings.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/prelude.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/settings/widgets/settings_user_view.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'base.dart'; + +extension AppFlowySettings on WidgetTester { + /// Open settings page + Future openSettings() async { + final settingsButton = find.byTooltip(LocaleKeys.settings_menu_open.tr()); + expect(settingsButton, findsOneWidget); + await tapButton(settingsButton); + final settingsDialog = find.byType(SettingsDialog); + expect(settingsDialog, findsOneWidget); + return; + } + + /// Open the page that insides the settings page + Future openSettingsPage(SettingsPage page) async { + final button = find.byWidgetPredicate( + (widget) => widget is SettingsMenuElement && widget.page == page, + ); + expect(button, findsOneWidget); + await tapButton(button); + return; + } + + /// Restore the AppFlowy data storage location + Future restoreLocation() async { + final button = + find.byTooltip(LocaleKeys.settings_files_recoverLocationTooltips.tr()); + expect(button, findsOneWidget); + await tapButton(button); + return; + } + + Future tapOpenFolderButton() async { + final button = find.text(LocaleKeys.settings_files_open.tr()); + expect(button, findsOneWidget); + await tapButton(button); + return; + } + + Future tapCustomLocationButton() async { + final button = find.byTooltip( + LocaleKeys.settings_files_changeLocationTooltips.tr(), + ); + expect(button, findsOneWidget); + await tapButton(button); + return; + } + + /// Enter user name + Future enterUserName(String name) async { + final uni = find.byType(UserNameInput); + expect(uni, findsOneWidget); + await tap(uni); + await enterText(uni, name); + await wait(300); // + await testTextInput.receiveAction(TextInputAction.done); + await pumpAndSettle(); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/util.dart b/frontend/appflowy_flutter/integration_test/util/util.dart new file mode 100644 index 0000000000..24efa3f5fb --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/util.dart @@ -0,0 +1,6 @@ +export 'base.dart'; +export 'common_operations.dart'; +export 'settings.dart'; +export 'data.dart'; +export 'expectation.dart'; +export 'editor_test_operations.dart'; diff --git a/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist b/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist index 7c56964006..8d4492f977 100644 --- a/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist +++ b/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 9.0 diff --git a/frontend/appflowy_flutter/ios/Podfile b/frontend/appflowy_flutter/ios/Podfile index 5e46cacdb4..1e8c3c90a5 100644 --- a/frontend/appflowy_flutter/ios/Podfile +++ b/frontend/appflowy_flutter/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -37,31 +37,5 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) - - target.build_configurations.each do |config| - config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ - '$(inherited)', - - # dart: PermissionGroup.photos - 'PERMISSION_PHOTOS=1', - ] - - end - end - - installer.aggregate_targets.each do |target| - target.xcconfigs.each do |variant, xcconfig| - xcconfig_path = target.client_root + target.xcconfig_relative_path(variant) - IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR")) - end - end - - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference - xcconfig_path = config.base_configuration_reference.real_path - IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR")) - end - end end end diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 4b7ed5d639..b79a4049b0 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -1,218 +1,81 @@ PODS: - - app_links (0.0.2): - - Flutter - - appflowy_backend (0.0.1): - - Flutter - - connectivity_plus (0.0.1): - - Flutter - - ReachabilitySwift - - device_info_plus (0.0.1): - - Flutter - - DKImagePickerController/Core (4.3.4): - - DKImagePickerController/ImageDataManager - - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.4) - - DKImagePickerController/PhotoGallery (4.3.4): - - DKImagePickerController/Core - - DKPhotoGallery - - DKImagePickerController/Resource (4.3.4) - - DKPhotoGallery (0.0.17): - - DKPhotoGallery/Core (= 0.0.17) - - DKPhotoGallery/Model (= 0.0.17) - - DKPhotoGallery/Preview (= 0.0.17) - - DKPhotoGallery/Resource (= 0.0.17) - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Core (0.0.17): - - DKPhotoGallery/Model - - DKPhotoGallery/Preview - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Model (0.0.17): - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Preview (0.0.17): - - DKPhotoGallery/Model - - DKPhotoGallery/Resource - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Resource (0.0.17): - - SDWebImage - - SwiftyGif - - file_picker (0.0.1): - - DKImagePickerController/PhotoGallery + - flowy_editor (0.0.1): - Flutter - flowy_infra_ui (0.0.1): - Flutter + - appflowy_backend (0.0.1): + - Flutter - Flutter (1.0.0) - - fluttertoast (0.0.2): + - flutter_inappwebview (0.0.1): - Flutter - - Toast - - image_picker_ios (0.0.1): + - flutter_inappwebview/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview/Core (0.0.1): - Flutter - - integration_test (0.0.1): + - OrderedSet (~> 5.0) + - flutter_keyboard_visibility (0.0.1): - Flutter - - irondash_engine_context (0.0.1): + - image_picker (0.0.1): - Flutter - - keyboard_height_plugin (0.0.1): + - OrderedSet (5.0.0) + - path_provider (0.0.1): - Flutter - - open_filex (0.0.2): + - url_launcher (0.0.1): - Flutter - - package_info_plus (0.4.5): + - video_player (0.0.1): - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - 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): - - Flutter - - FlutterMacOS - - super_native_extensions (0.0.1): - - Flutter - - SwiftyGif (5.4.3) - - 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`) - - appflowy_backend (from `.symlinks/plugins/appflowy_backend/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - - file_picker (from `.symlinks/plugins/file_picker/ios`) + - flowy_editor (from `.symlinks/plugins/flowy_editor/ios`) - flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`) + - appflowy_backend (from `.symlinks/plugins/appflowy_backend/ios`) - Flutter (from `Flutter`) - - fluttertoast (from `.symlinks/plugins/fluttertoast/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`) - - 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`) + - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - image_picker (from `.symlinks/plugins/image_picker/ios`) + - path_provider (from `.symlinks/plugins/path_provider/ios`) + - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + - video_player (from `.symlinks/plugins/video_player/ios`) SPEC REPOS: trunk: - - DKImagePickerController - - DKPhotoGallery - - ReachabilitySwift - - SDWebImage - - Sentry - - SwiftyGif - - Toast + - OrderedSet EXTERNAL SOURCES: - app_links: - :path: ".symlinks/plugins/app_links/ios" - appflowy_backend: - :path: ".symlinks/plugins/appflowy_backend/ios" - connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" - device_info_plus: - :path: ".symlinks/plugins/device_info_plus/ios" - file_picker: - :path: ".symlinks/plugins/file_picker/ios" + flowy_editor: + :path: ".symlinks/plugins/flowy_editor/ios" flowy_infra_ui: :path: ".symlinks/plugins/flowy_infra_ui/ios" + appflowy_backend: + :path: ".symlinks/plugins/appflowy_backend/ios" Flutter: :path: Flutter - fluttertoast: - :path: ".symlinks/plugins/fluttertoast/ios" - image_picker_ios: - :path: ".symlinks/plugins/image_picker_ios/ios" - integration_test: - :path: ".symlinks/plugins/integration_test/ios" - irondash_engine_context: - :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" - 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" + flutter_inappwebview: + :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + image_picker: + :path: ".symlinks/plugins/image_picker/ios" + path_provider: + :path: ".symlinks/plugins/path_provider/ios" + url_launcher: + :path: ".symlinks/plugins/url_launcher/ios" + video_player: + :path: ".symlinks/plugins/video_player/ios" SPEC CHECKSUMS: - app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 - appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 - DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac - DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 - 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 - 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 - SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + flowy_editor: bf8d58894ddb03453bd4d8521c57267ad638b837 + flowy_infra_ui: 146c88346fd55d2ee6a41ae35059a5bf095cfbb3 + appflowy_backend: c416222c639e678828776789bf0c1a1d0d59df3c + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + image_picker: 9aa50e1d8cdacdbed739e925b7eea16d014367e6 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + video_player: ecd305f42e9044793efd34846e1ce64c31ea6fcb -PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c -COCOAPODS: 1.16.2 +COCOAPODS: 1.10.1 diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index 804ad052be..385c983d7f 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -13,9 +13,22 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - FB3C2A642AE0D57700490715 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 197F72694BED43249F1523E8 /* Pods_Runner.framework */; }; + 9D1D47ADD7F5DE8237063BCA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 197F72694BED43249F1523E8 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; @@ -34,7 +47,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - FBE00AD62AE8E46A006B563F /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -42,7 +54,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FB3C2A642AE0D57700490715 /* Pods_Runner.framework in Frameworks */, + 9D1D47ADD7F5DE8237063BCA /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,7 +102,6 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - FBE00AD62AE8E46A006B563F /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -110,6 +121,7 @@ 580A1ED8E012CA1552E5EFD3 /* Pods-Runner.release.xcconfig */, 4C2CB38DA64605A62D45B098 /* Pods-Runner.profile.xcconfig */, ); + name = Pods; path = Pods; sourceTree = ""; }; @@ -125,9 +137,9 @@ 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 08FAA63113168DEC7FB74204 /* [CP] Embed Pods Frameworks */, - A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -144,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -205,12 +217,10 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -221,7 +231,6 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -234,23 +243,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; E790B8FE5609053209ED85CB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -348,7 +340,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; @@ -363,32 +355,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEAD_CODE_STRIPPING = NO; - DEVELOPMENT_TEAM = VHB67HRSZG; 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)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - 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; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -440,7 +414,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; @@ -489,7 +463,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; @@ -505,33 +479,15 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEAD_CODE_STRIPPING = NO; - DEVELOPMENT_TEAM = VHB67HRSZG; 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)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - 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; - TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -542,32 +498,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEAD_CODE_STRIPPING = NO; - DEVELOPMENT_TEAM = VHB67HRSZG; 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)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - 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; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e67b2808af..3db53b6e1f 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - - CFBundleName - AppFlowy - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLName - - CFBundleURLSchemes - - appflowy-flutter - - - - 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 - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - - UISupportsDocumentBrowser - - UIViewControllerBasedStatusBarAppearance - - - \ No newline at end of file + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + appflowy_flutter + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CFBundleLocalizations + + en + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/frontend/appflowy_flutter/ios/Runner/Runner.entitlements b/frontend/appflowy_flutter/ios/Runner/Runner.entitlements deleted file mode 100644 index e3bc137465..0000000000 --- a/frontend/appflowy_flutter/ios/Runner/Runner.entitlements +++ /dev/null @@ -1,19 +0,0 @@ - - - - - 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/config.dart b/frontend/appflowy_flutter/lib/core/config/config.dart new file mode 100644 index 0000000000..1c727f78f2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/core/config/config.dart @@ -0,0 +1,38 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart'; + +class Config { + static Future setSupabaseConfig({ + required String url, + required String anonKey, + required String key, + required String secret, + }) async { + await ConfigEventSetSupabaseConfig( + SupabaseConfigPB.create() + ..supabaseUrl = url + ..key = key + ..anonKey = anonKey + ..jwtSecret = secret, + ).send(); + } + + static Future setSupabaseCollabPluginConfig({ + required String url, + required String key, + required String jwtSecret, + required String collabTable, + }) async { + final payload = CollabPluginConfigPB.create(); + final collabTableConfig = CollabTableConfigPB.create() + ..tableName = collabTable; + + payload.supabaseConfig = SupabaseDBConfigPB.create() + ..supabaseUrl = url + ..key = key + ..jwtSecret = jwtSecret + ..collabTableConfig = collabTableConfig; + + await ConfigEventSetCollabPluginConfig(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/core/config/kv.dart b/frontend/appflowy_flutter/lib/core/config/kv.dart index b7c845b547..8ed337de51 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv.dart @@ -1,12 +1,12 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; import 'package:shared_preferences/shared_preferences.dart'; abstract class KeyValueStorage { Future set(String key, String value); - Future get(String key); - Future getWithFormat( - String key, - T Function(String value) formatter, - ); + Future> get(String key); Future remove(String key); Future clear(); } @@ -16,26 +16,14 @@ class DartKeyValue implements KeyValueStorage { SharedPreferences get sharedPreferences => _sharedPreferences!; @override - Future get(String key) async { + Future> get(String key) async { await _initSharedPreferencesIfNeeded(); final value = sharedPreferences.getString(key); if (value != null) { - return value; + return Right(value); } - return null; - } - - @override - Future getWithFormat( - String key, - T Function(String value) formatter, - ) async { - final value = await get(key); - if (value == null) { - return null; - } - return formatter(value); + return Left(FlowyError()); } @override @@ -63,3 +51,35 @@ class DartKeyValue implements KeyValueStorage { _sharedPreferences ??= await SharedPreferences.getInstance(); } } + +/// Key-value store +/// The data is stored in the local storage of the device. +class RustKeyValue implements KeyValueStorage { + @override + Future set(String key, String value) async { + await ConfigEventSetKeyValue( + KeyValuePB.create() + ..key = key + ..value = value, + ).send(); + } + + @override + Future> get(String key) async { + final payload = KeyPB.create()..key = key; + final response = await ConfigEventGetKeyValue(payload).send(); + return response.swap().map((r) => r.value); + } + + @override + Future remove(String key) async { + await ConfigEventRemoveKeyValue( + KeyPB.create()..key = key, + ).send(); + } + + @override + Future clear() async { + // TODO(Lucas): implement clear + } +} diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index aefd5e5d36..9962980e50 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -6,118 +6,16 @@ class KVKeys { /// The key for the path location of the local data for the whole app. static const String pathLocation = '$prefix.path_location'; + /// The key for the last time login type. + /// + /// The value is one of the following: + /// - local + /// - supabase + static const String loginType = '$prefix.login_type'; + /// The key for saving the window size /// /// The value is a json string with the following format: /// {'height': 600.0, 'width': 800.0} static const String windowSize = 'windowSize'; - - /// The key for saving the window position - /// - /// The value is a json string with the following format: - /// {'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 = - 'kDocumentAppearanceFontFamily'; - static const String kDocumentAppearanceDefaultTextDirection = - 'kDocumentAppearanceDefaultTextDirection'; - static const String kDocumentAppearanceCursorColor = - 'kDocumentAppearanceCursorColor'; - static const String kDocumentAppearanceSelectionColor = - 'kDocumentAppearanceSelectionColor'; - static const String kDocumentAppearanceWidth = 'kDocumentAppearanceWidth'; - - /// The key for saving the expanded views - /// - /// The value is a json string with the following format: - /// {'viewId': true, 'viewId2': false} - static const String expandedViews = 'expandedViews'; - - /// The key for saving the expanded folder - /// - /// The value is a json string with the following format: - /// {'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. - static const String showRenameDialogWhenCreatingNewFile = - 'showRenameDialogWhenCreatingNewFile'; - - static const String kCloudType = 'kCloudType'; - static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL'; - static const String kAppFlowyBaseShareDomain = 'kAppFlowyBaseShareDomain'; - static const String kAppFlowyEnableSyncTrace = 'kAppFlowyEnableSyncTrace'; - - /// The key for saving the text scale factor. - /// - /// The value is a double string. - /// The value range is from 0.8 to 1.0. If it's greater than 1.0, it will cause - /// the text to be too large and not aligned with the icon - static const String textScaleFactor = 'textScaleFactor'; - - /// The key for saving the feature flags - /// - /// The value is a json string with the following format: - /// {'feature_flag_1': true, 'feature_flag_2': false} - static const String featureFlag = 'featureFlag'; - - /// The key for saving show notification icon option - /// - /// The value is a boolean string - static const String showNotificationIcon = 'showNotificationIcon'; - - /// The key for saving the last opened workspace id - /// - /// The workspace id is a string. - @Deprecated('deprecated in version 0.5.5') - static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId'; - - /// The key for saving the scale factor - /// - /// The value is a double string. - static const String scaleFactor = 'scaleFactor'; - - /// The key for saving the last opened tab (favorite, recent, space etc.) - /// - /// The value is a int string. - static const String lastOpenedSpace = 'lastOpenedSpace'; - - /// The key for saving the space tab order - /// - /// The value is a json string with the following format: - /// [0, 1, 2] - static const String spaceOrder = 'spaceOrder'; - - /// The key for saving the last opened space id (space A, space B) - /// - /// The value is a string. - static const String lastOpenedSpaceId = 'lastOpenedSpaceId'; - - /// The key for saving the upgrade space tag - /// - /// 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..3641aeef4d 100644 --- a/frontend/appflowy_flutter/lib/core/frameless_window.dart +++ b/frontend/appflowy_flutter/lib/core/frameless_window.dart @@ -1,7 +1,6 @@ -import 'package:appflowy/startup/tasks/device_info_task.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:universal_platform/universal_platform.dart'; +import 'package:flutter/material.dart'; +import 'dart:io' show Platform; class CocoaWindowChannel { CocoaWindowChannel._(); @@ -27,10 +26,7 @@ class CocoaWindowChannel { } class MoveWindowDetector extends StatefulWidget { - const MoveWindowDetector({ - super.key, - this.child, - }); + const MoveWindowDetector({Key? key, this.child}) : super(key: key); final Widget? child; @@ -44,21 +40,15 @@ class MoveWindowDetectorState extends State { @override Widget build(BuildContext context) { - // the frameless window is only supported on macOS - if (!UniversalPlatform.isMacOS) { - return widget.child ?? const SizedBox.shrink(); + if (!Platform.isMacOS) { + return widget.child ?? Container(); } - - // 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(); - } - return GestureDetector( // https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack behavior: HitTestBehavior.translucent, - onDoubleTap: () async => CocoaWindowChannel.instance.zoom(), + onDoubleTap: () async { + await CocoaWindowChannel.instance.zoom(); + }, onPanStart: (DragStartDetails details) { winX = details.globalPosition.dx; winY = details.globalPosition.dy; diff --git a/frontend/appflowy_flutter/lib/core/helpers/helpers.dart b/frontend/appflowy_flutter/lib/core/helpers/helpers.dart index e325b1a7bb..fd832306e8 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/helpers.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/helpers.dart @@ -1 +1,2 @@ export 'target_platform.dart'; +export 'url_validator.dart'; diff --git a/frontend/appflowy_flutter/lib/core/helpers/target_platform.dart b/frontend/appflowy_flutter/lib/core/helpers/target_platform.dart index ba50353a6e..5b5419dfd9 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/target_platform.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/target_platform.dart @@ -1,12 +1,11 @@ -import 'package:flutter/foundation.dart' show TargetPlatform, kIsWeb; +import 'package:flutter/foundation.dart' show TargetPlatform; extension TargetPlatformHelper on TargetPlatform { /// Convenience function to check if the app is running on a desktop computer. /// /// Easily check if on desktop by checking `defaultTargetPlatform.isDesktop`. bool get isDesktop => - !kIsWeb && - (this == TargetPlatform.linux || - this == TargetPlatform.macOS || - this == TargetPlatform.windows); + this == TargetPlatform.linux || + this == TargetPlatform.macOS || + this == TargetPlatform.windows; } diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart deleted file mode 100644 index 0502e79604..0000000000 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ /dev/null @@ -1,168 +0,0 @@ -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: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( - Uri uri, { - BuildContext? context, - OnFailureCallback? onFailure, - launcher.LaunchMode mode = launcher.LaunchMode.platformDefault, - 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( - uri, - context: context, - onFailure: onFailure, - ); - } - - // 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'); - } - - // try to launch the uri directly - bool result = await launcher.canLaunchUrl(uri); - if (result) { - try { - result = await launcher.launchUrl( - uri, - mode: mode, - webOnlyWindowName: webOnlyWindowName, - ); - } 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 - - if (addingHttpSchemeWhenFailed && - !result && - !isURL(url, {'require_protocol': true})) { - try { - final uriWithScheme = Uri.parse('http://$url'); - result = await launcher.launchUrl( - uriWithScheme, - mode: mode, - webOnlyWindowName: webOnlyWindowName, - ); - } on PlatformException catch (e) { - Log.error('Failed to open uri: $e'); - if (context != null && context.mounted) { - _errorHandler(uri, context: context, onFailure: onFailure, e: e); - } - } - } - - 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 { - uri = Uri.parse(url); - } on FormatException catch (e) { - Log.error('Failed to parse url: $e'); - return false; - } - - // try to launch the uri directly - return afLaunchUri( - 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, - OnFailureCallback? onFailure, - PlatformException? e, -}) { - Log.error('Failed to open uri: $e'); - - if (onFailure != null) { - onFailure(uri); - } else { - showMessageToast( - LocaleKeys.failedToOpenUrl.tr(args: [e?.message ?? "PlatformException"]), - context: context, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_validator.dart b/frontend/appflowy_flutter/lib/core/helpers/url_validator.dart new file mode 100644 index 0000000000..bfead5619d --- /dev/null +++ b/frontend/appflowy_flutter/lib/core/helpers/url_validator.dart @@ -0,0 +1,21 @@ +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'url_validator.freezed.dart'; + +Either parseValidUrl(String url) { + try { + final uri = Uri.parse(url); + if (uri.scheme.isEmpty || uri.host.isEmpty) { + return left(const UriFailure.invalidSchemeHost()); + } + return right(uri); + } on FormatException { + return left(const UriFailure.invalidUriFormat()); + } +} + +@freezed +class UriFailure with _$UriFailure { + const factory UriFailure.invalidSchemeHost() = _InvalidSchemeHost; + const factory UriFailure.invalidUriFormat() = _InvalidUriFormat; +} diff --git a/frontend/appflowy_flutter/lib/core/network_monitor.dart b/frontend/appflowy_flutter/lib/core/network_monitor.dart index 3d01204921..1aee637176 100644 --- a/frontend/appflowy_flutter/lib/core/network_monitor.dart +++ b/frontend/appflowy_flutter/lib/core/network_monitor.dart @@ -1,34 +1,34 @@ 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:appflowy_backend/protobuf/flowy-net/network_state.pb.dart'; import 'package:flutter/services.dart'; class NetworkListener { + final Connectivity _connectivity = Connectivity(); + late StreamSubscription _connectivitySubscription; + NetworkListener() { _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); } - final Connectivity _connectivity = Connectivity(); - late StreamSubscription _connectivitySubscription; - Future start() async { late ConnectivityResult result; // Platform messages may fail, so we use a try/catch PlatformException. try { result = await _connectivity.checkConnectivity(); } on PlatformException catch (e) { - Log.error("Couldn't check connectivity status. $e"); + Log.error('Couldn\'t check connectivity status. $e'); return; } return _updateConnectionStatus(result); } - Future stop() async { - await _connectivitySubscription.cancel(); + void stop() { + _connectivitySubscription.cancel(); } Future _updateConnectionStatus(ConnectivityResult result) async { @@ -46,12 +46,16 @@ class NetworkListener { return NetworkTypePB.VPN; case ConnectivityResult.none: case ConnectivityResult.other: - return NetworkTypePB.NetworkUnknown; + return NetworkTypePB.Unknown; } }(); + Log.info("Network type: $networkType"); final state = NetworkStatePB.create()..ty = networkType; - return UserEventUpdateNetworkState(state).send().then((result) { - result.fold((l) {}, (e) => Log.error(e)); + NetworkEventUpdateNetworkType(state).send().then((result) { + result.fold( + (l) {}, + (e) => Log.error(e), + ); }); } } diff --git a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart index afe850f88f..bffcbf2a72 100644 --- a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart @@ -1,18 +1,24 @@ -import 'package:appflowy/core/notification/notification_helper.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'dart:typed_data'; -// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value -const String _source = 'Document'; +import 'package:appflowy/core/notification/notification_helper.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; + +typedef DocumentNotificationCallback = void Function( + DocumentNotification, + Either, +); class DocumentNotificationParser extends NotificationParser { DocumentNotificationParser({ - super.id, - required super.callback, + String? id, + required DocumentNotificationCallback callback, }) : super( - tyParser: (ty, source) => - source == _source ? DocumentNotification.valueOf(ty) : null, + id: id, + callback: callback, + tyParser: (ty) => DocumentNotification.valueOf(ty), errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } diff --git a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart index a5b99484bc..5b33670057 100644 --- a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart @@ -1,35 +1,41 @@ import 'dart:async'; import 'dart:typed_data'; - -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; -// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value -const String _source = 'Workspace'; +// Folder +typedef FolderNotificationCallback = void Function( + FolderNotification, + Either, +); class FolderNotificationParser extends NotificationParser { FolderNotificationParser({ - super.id, - required super.callback, + String? id, + required FolderNotificationCallback callback, }) : super( - tyParser: (ty, source) => - source == _source ? FolderNotification.valueOf(ty) : null, + id: id, + callback: callback, + tyParser: (ty) => FolderNotification.valueOf(ty), errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } typedef FolderNotificationHandler = Function( FolderNotification ty, - FlowyResult result, + Either result, ); class FolderNotificationListener { + StreamSubscription? _subscription; + FolderNotificationParser? _parser; + FolderNotificationListener({ required String objectId, required FolderNotificationHandler handler, @@ -41,9 +47,6 @@ class FolderNotificationListener { RustStreamReceiver.listen((observable) => _parser?.parse(observable)); } - FolderNotificationParser? _parser; - StreamSubscription? _subscription; - Future stop() async { _parser = null; await _subscription?.cancel(); diff --git a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart index d5425a042d..fd2ce352f8 100644 --- a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart @@ -1,35 +1,41 @@ import 'dart:async'; import 'dart:typed_data'; - -import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; -// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value -const String _source = 'Database'; +// DatabasePB +typedef DatabaseNotificationCallback = void Function( + DatabaseNotification, + Either, +); class DatabaseNotificationParser extends NotificationParser { DatabaseNotificationParser({ - super.id, - required super.callback, + String? id, + required DatabaseNotificationCallback callback, }) : super( - tyParser: (ty, source) => - source == _source ? DatabaseNotification.valueOf(ty) : null, + id: id, + callback: callback, + tyParser: (ty) => DatabaseNotification.valueOf(ty), errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } typedef DatabaseNotificationHandler = Function( DatabaseNotification ty, - FlowyResult result, + Either result, ); class DatabaseNotificationListener { + StreamSubscription? _subscription; + DatabaseNotificationParser? _parser; + DatabaseNotificationListener({ required String objectId, required DatabaseNotificationHandler handler, @@ -38,9 +44,6 @@ class DatabaseNotificationListener { RustStreamReceiver.listen((observable) => _parser?.parse(observable)); } - DatabaseNotificationParser? _parser; - StreamSubscription? _subscription; - Future stop() async { _parser = null; await _subscription?.cancel(); diff --git a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart index e6ed20fab0..4869fb14d4 100644 --- a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart +++ b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart @@ -1,21 +1,20 @@ import 'dart:typed_data'; - import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:dartz/dartz.dart'; + +class NotificationParser { + String? id; + void Function(T, Either) callback; + + T? Function(int) tyParser; + E Function(Uint8List) errorParser; -class NotificationParser { NotificationParser({ this.id, required this.callback, required this.errorParser, required this.tyParser, }); - - String? id; - void Function(T, FlowyResult) callback; - E Function(Uint8List) errorParser; - T? Function(int, String) tyParser; - void parse(SubscribeObject subject) { if (id != null) { if (subject.id != id) { @@ -23,7 +22,7 @@ class NotificationParser { } } - final ty = tyParser(subject.ty, subject.source); + final ty = tyParser(subject.ty); if (ty == null) { return; } @@ -31,10 +30,10 @@ class NotificationParser { if (subject.hasError()) { final bytes = Uint8List.fromList(subject.error); final error = errorParser(bytes); - callback(ty, FlowyResult.failure(error)); + callback(ty, right(error)); } else { final bytes = Uint8List.fromList(subject.payload); - callback(ty, FlowyResult.success(bytes)); + callback(ty, left(bytes)); } } } diff --git a/frontend/appflowy_flutter/lib/core/notification/search_notification.dart b/frontend/appflowy_flutter/lib/core/notification/search_notification.dart deleted file mode 100644 index 18f9b218f1..0000000000 --- a/frontend/appflowy_flutter/lib/core/notification/search_notification.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.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-search/notification.pbenum.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -import 'notification_helper.dart'; - -// This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE) -const _source = 'Search'; - -class SearchNotificationParser - extends NotificationParser { - SearchNotificationParser({ - super.id, - required super.callback, - String? channel, - }) : super( - tyParser: (ty, source) => source == "$_source$channel" - ? SearchNotification.valueOf(ty) - : null, - errorParser: (bytes) => FlowyError.fromBuffer(bytes), - ); -} - -typedef SearchNotificationHandler = Function( - SearchNotification ty, - FlowyResult result, -); - -class SearchNotificationListener { - SearchNotificationListener({ - required String objectId, - required SearchNotificationHandler handler, - String? channel, - }) : _parser = SearchNotificationParser( - id: objectId, - callback: handler, - channel: channel, - ) { - _subscription = - RustStreamReceiver.listen((observable) => _parser?.parse(observable)); - } - - StreamSubscription? _subscription; - SearchNotificationParser? _parser; - - Future stop() async { - _parser = null; - await _subscription?.cancel(); - _subscription = null; - } -} diff --git a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart index c9582c3dda..1da65d12c6 100644 --- a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart @@ -1,19 +1,51 @@ -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/rust_stream.dart'; import 'notification_helper.dart'; -// This value should be the same as the USER_OBSERVABLE_SOURCE value -const String _source = 'User'; +// User +typedef UserNotificationCallback = void Function( + UserNotification, + Either, +); class UserNotificationParser extends NotificationParser { UserNotificationParser({ - required String super.id, - required super.callback, + required String id, + required UserNotificationCallback callback, }) : super( - tyParser: (ty, source) => - source == _source ? UserNotification.valueOf(ty) : null, + id: id, + callback: callback, + tyParser: (ty) => UserNotification.valueOf(ty), errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } + +typedef UserNotificationHandler = Function( + UserNotification ty, + Either result, +); + +class UserNotificationListener { + StreamSubscription? _subscription; + UserNotificationParser? _parser; + + UserNotificationListener({ + required String objectId, + required UserNotificationHandler handler, + }) : _parser = UserNotificationParser(id: objectId, callback: handler) { + _subscription = + RustStreamReceiver.listen((observable) => _parser?.parse(observable)); + } + + Future stop() async { + _parser = null; + await _subscription?.cancel(); + } +} diff --git a/frontend/appflowy_flutter/lib/date/date_service.dart b/frontend/appflowy_flutter/lib/date/date_service.dart deleted file mode 100644 index bf49bce7a5..0000000000 --- a/frontend/appflowy_flutter/lib/date/date_service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-date/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -class DateService { - static Future> queryDate( - String search, - ) async { - final query = DateQueryPB.create()..query = search; - final result = await DateEventQueryDate(query).send(); - return result.fold( - (s) { - final date = DateTime.tryParse(s.date); - if (date != null) { - return FlowyResult.success(date); - } - return FlowyResult.failure( - FlowyError(msg: 'Could not parse Date (NLP) from String'), - ); - }, - (e) => FlowyResult.failure(e), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/env/backend_env.dart b/frontend/appflowy_flutter/lib/env/backend_env.dart deleted file mode 100644 index eb8a61d037..0000000000 --- a/frontend/appflowy_flutter/lib/env/backend_env.dart +++ /dev/null @@ -1,80 +0,0 @@ -// 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() -class AppFlowyConfiguration { - AppFlowyConfiguration({ - required this.root, - required this.app_version, - required this.custom_app_path, - required this.origin_app_path, - required this.device_id, - required this.platform, - required this.authenticator_type, - required this.appflowy_cloud_config, - required this.envs, - }); - - factory AppFlowyConfiguration.fromJson(Map json) => - _$AppFlowyConfigurationFromJson(json); - - final String root; - final String app_version; - final String custom_app_path; - final String origin_app_path; - final String device_id; - final String platform; - final int authenticator_type; - final AppFlowyCloudConfiguration appflowy_cloud_config; - final Map envs; - - Map toJson() => _$AppFlowyConfigurationToJson(this); -} - -@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) => - _$AppFlowyCloudConfigurationFromJson(json); - - 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); - - static AppFlowyCloudConfiguration defaultConfig() { - return AppFlowyCloudConfiguration( - base_url: '', - ws_base_url: '', - gotrue_url: '', - enable_sync_trace: false, - base_web_domain: ShareConstants.defaultBaseWebDomain, - ); - } - - bool get isValid { - return base_url.isNotEmpty && - ws_base_url.isNotEmpty && - gotrue_url.isNotEmpty; - } -} diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart deleted file mode 100644 index 15f3ada42e..0000000000 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ /dev/null @@ -1,344 +0,0 @@ -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'; - -/// Sets the cloud type for the application. -/// -/// This method updates the cloud type setting in the key-value storage -/// using the [KeyValueStorage] service. The cloud type is identified -/// by the [AuthenticatorType] enum. -/// -/// [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.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.appflowyCloud: - await getIt().set(KVKeys.kCloudType, 2.toString()); - break; - case AuthenticatorType.appflowyCloudSelfHost: - await getIt().set(KVKeys.kCloudType, 3.toString()); - break; - case AuthenticatorType.appflowyCloudDevelop: - await getIt().set(KVKeys.kCloudType, 4.toString()); - break; - } -} - -const String kAppflowyCloudUrl = "https://beta.appflowy.cloud"; - -/// Retrieves the currently set cloud type. -/// -/// This method fetches the cloud type setting from the key-value storage -/// using the [KeyValueStorage] service and returns the corresponding -/// [AuthenticatorType] enum value. -/// -/// Returns: -/// A Future that resolves to a [AuthenticatorType] enum value representing the -/// currently set cloud type. The default return value is `CloudType.local` -/// if no valid setting is found. -/// -Future getAuthenticatorType() async { - final value = await getIt().get(KVKeys.kCloudType); - if (value == null && !integrationMode().isUnitTest) { - // if the cloud type is not set, then set it to AppFlowy Cloud as default. - await useAppFlowyBetaCloudWithURL( - kAppflowyCloudUrl, - AuthenticatorType.appflowyCloud, - ); - return AuthenticatorType.appflowyCloud; - } - - switch (value ?? "0") { - case "0": - return AuthenticatorType.local; - case "2": - return AuthenticatorType.appflowyCloud; - case "3": - return AuthenticatorType.appflowyCloudSelfHost; - case "4": - return AuthenticatorType.appflowyCloudDevelop; - default: - await useAppFlowyBetaCloudWithURL( - kAppflowyCloudUrl, - AuthenticatorType.appflowyCloud, - ); - return AuthenticatorType.appflowyCloud; - } -} - -/// Determines whether authentication is enabled. -/// -/// This getter evaluates if authentication should be enabled based on the -/// current integration mode and cloud type settings. -/// -/// Returns: -/// 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. -/// Returns `false` otherwise. -bool get isAuthEnabled { - final env = getIt(); - if (env.authenticatorType.isAppFlowyCloudEnabled) { - return env.appflowyCloudConfig.isValid; - } - - return false; -} - -bool get isLocalAuthEnabled { - return currentCloudType().isLocal; -} - -/// Determines if AppFlowy Cloud is enabled. -bool get isAppFlowyCloudEnabled { - return currentCloudType().isAppFlowyCloudEnabled; -} - -enum AuthenticatorType { - local, - appflowyCloud, - appflowyCloudSelfHost, - // The 'appflowyCloudDevelop' type is used for develop purposes only. - appflowyCloudDevelop; - - bool get isLocal => this == AuthenticatorType.local; - - bool get isAppFlowyCloudEnabled => - this == AuthenticatorType.appflowyCloudSelfHost || - this == AuthenticatorType.appflowyCloudDevelop || - this == AuthenticatorType.appflowyCloud; - - int get value { - switch (this) { - case AuthenticatorType.local: - return 0; - case AuthenticatorType.appflowyCloud: - return 2; - case AuthenticatorType.appflowyCloudSelfHost: - return 3; - case AuthenticatorType.appflowyCloudDevelop: - return 4; - } - } - - static AuthenticatorType fromValue(int value) { - switch (value) { - case 0: - return AuthenticatorType.local; - case 2: - return AuthenticatorType.appflowyCloud; - case 3: - return AuthenticatorType.appflowyCloudSelfHost; - case 4: - return AuthenticatorType.appflowyCloudDevelop; - default: - return AuthenticatorType.local; - } - } -} - -AuthenticatorType currentCloudType() { - return getIt().authenticatorType; -} - -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); -} - -Future useAppFlowyBetaCloudWithURL( - String url, - AuthenticatorType authenticatorType, -) async { - await _setAuthenticatorType(authenticatorType); - await _setAppFlowyCloudUrl(url); -} - -Future useLocalServer() async { - await _setAuthenticatorType(AuthenticatorType.local); -} - -// Use getIt() to get the shared environment. -class AppFlowyCloudSharedEnv { - AppFlowyCloudSharedEnv({ - required AuthenticatorType authenticatorType, - required this.appflowyCloudConfig, - }) : _authenticatorType = authenticatorType; - - final AuthenticatorType _authenticatorType; - final AppFlowyCloudConfiguration appflowyCloudConfig; - - AuthenticatorType get authenticatorType => _authenticatorType; - - static Future fromEnv() async { - // If [Env.enableCustomCloud] is true, then use the custom cloud configuration. - if (Env.enableCustomCloud) { - // Use the custom cloud configuration. - var authenticatorType = await getAuthenticatorType(); - - final appflowyCloudConfig = authenticatorType.isAppFlowyCloudEnabled - ? await getAppFlowyCloudConfig(authenticatorType) - : AppFlowyCloudConfiguration.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 - // converted to [AuthenticatorType.appflowyCloud] to align with the backend representation, - // where both types are indicated by the value '2'. - if (authenticatorType.isAppFlowyCloudEnabled) { - authenticatorType = AuthenticatorType.appflowyCloud; - } - return AppFlowyCloudSharedEnv( - authenticatorType: authenticatorType, - appflowyCloudConfig: appflowyCloudConfig, - ); - } else { - // Using the cloud settings from the .env file. - final appflowyCloudConfig = AppFlowyCloudConfiguration( - 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, - ); - } - } - - @override - String toString() { - return 'authenticator: $_authenticatorType\n' - 'appflowy: ${appflowyCloudConfig.toJson()}\n'; - } -} - -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, - ); - } -} - -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, - ); - } catch (e) { - Log.error("Failed to parse AppFlowy Cloud URL: $e"); - return AppFlowyCloudConfiguration.defaultConfig(); - } -} - -Future getAppFlowyCloudUrl() async { - final result = - await getIt().get(KVKeys.kAppflowyCloudBaseURL); - 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); - - // Construct the WebSocket URL directly from the parsed URI. - final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws'; - final wsUrl = - Uri(scheme: wsScheme, host: uri.host, port: uri.port, path: '/ws/v1'); - - return wsUrl.toString(); - } catch (e) { - Log.error("Failed to get WebSocket URL: $e"); - return ""; - } -} - -Future _getAppFlowyCloudGotrueUrl(String baseURL) async { - return "$baseURL/gotrue"; -} diff --git a/frontend/appflowy_flutter/lib/env/cloud_env_test.dart b/frontend/appflowy_flutter/lib/env/cloud_env_test.dart deleted file mode 100644 index 87867f6d7e..0000000000 --- a/frontend/appflowy_flutter/lib/env/cloud_env_test.dart +++ /dev/null @@ -1,33 +0,0 @@ -// lib/env/env.dart -// ignore_for_file: prefer_const_declarations - -import 'package:envied/envied.dart'; - -part 'cloud_env_test.g.dart'; - -/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider. -/// -@Envied(path: '.env.cloud.test') -abstract class TestEnv { - /// AppFlowy Cloud Configuration - @EnviedField( - obfuscate: false, - varName: 'APPFLOWY_CLOUD_URL', - defaultValue: 'http://localhost', - ) - static final String afCloudUrl = _TestEnv.afCloudUrl; - - // Supabase Configuration: - @EnviedField( - obfuscate: false, - varName: 'SUPABASE_URL', - defaultValue: '', - ) - static final String supabaseUrl = _TestEnv.supabaseUrl; - @EnviedField( - obfuscate: false, - varName: 'SUPABASE_ANON_KEY', - defaultValue: '', - ) - static final String supabaseAnonKey = _TestEnv.supabaseAnonKey; -} diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart index 18434f9aa6..c52f2f0b13 100644 --- a/frontend/appflowy_flutter/lib/env/env.dart +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -1,54 +1,45 @@ // 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'; @Envied(path: '.env') abstract class Env { - // This flag is used to decide if users can dynamically configure cloud settings. It turns true when a .env file exists containing the APPFLOWY_CLOUD_URL variable. By default, this is set to false. - static bool get enableCustomCloud { - return Env.authenticatorType == - AuthenticatorType.appflowyCloudSelfHost.value || - Env.authenticatorType == AuthenticatorType.appflowyCloud.value || - Env.authenticatorType == AuthenticatorType.appflowyCloudDevelop.value && - _Env.afCloudUrl.isEmpty; - } - @EnviedField( - obfuscate: false, - varName: 'AUTHENTICATOR_TYPE', - defaultValue: 2, - ) - static const int authenticatorType = _Env.authenticatorType; - - /// AppFlowy Cloud Configuration - @EnviedField( - obfuscate: false, - varName: 'APPFLOWY_CLOUD_URL', + obfuscate: true, + varName: 'SUPABASE_URL', defaultValue: '', ) - static const String afCloudUrl = _Env.afCloudUrl; - + static final String supabaseUrl = _Env.supabaseUrl; @EnviedField( - obfuscate: false, - varName: 'INTERNAL_BUILD', + obfuscate: true, + varName: 'SUPABASE_ANON_KEY', defaultValue: '', ) - static const String internalBuild = _Env.internalBuild; - + static final String supabaseAnonKey = _Env.supabaseAnonKey; @EnviedField( - obfuscate: false, - varName: 'SENTRY_DSN', + obfuscate: true, + varName: 'SUPABASE_KEY', defaultValue: '', ) - static const String sentryDsn = _Env.sentryDsn; + static final String supabaseKey = _Env.supabaseKey; + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_JWT_SECRET', + defaultValue: '', + ) + static final String supabaseJwtSecret = _Env.supabaseJwtSecret; @EnviedField( - obfuscate: false, - varName: 'BASE_WEB_DOMAIN', - defaultValue: ShareConstants.defaultBaseWebDomain, + obfuscate: true, + varName: 'SUPABASE_COLLAB_TABLE', + defaultValue: '', ) - static const String baseWebDomain = _Env.baseWebDomain; + static final String supabaseCollabTable = _Env.supabaseCollabTable; } + +bool get isSupabaseEnable => + Env.supabaseUrl.isNotEmpty && + Env.supabaseAnonKey.isNotEmpty && + Env.supabaseKey.isNotEmpty && + Env.supabaseJwtSecret.isNotEmpty; diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart deleted file mode 100644 index 157be012b1..0000000000 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ /dev/null @@ -1,1065 +0,0 @@ -// 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. - -// TODO(Mathias): Make a PR in Flutter repository that enables customizing -// the dropdown menu without having to copy the entire file. -// This is a temporary solution! - -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; - -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 = - { - LogicalKeySet(LogicalKeyboardKey.arrowUp): const _ArrowUpIntent(), - LogicalKeySet(LogicalKeyboardKey.arrowDown): const _ArrowDownIntent(), -}; - -/// A dropdown menu that can be opened from a [TextField]. The selected -/// menu item is displayed in that field. -/// -/// This widget is used to help people make a choice from a menu and put the -/// selected item into the text input field. People can also filter the list based -/// on the text input or search one item in the menu list. -/// -/// The menu is composed of a list of [DropdownMenuEntry]s. People can provide information, -/// such as: label, leading icon or trailing icon for each entry. The [TextField] -/// will be updated based on the selection from the menu entries. The text field -/// will stay empty if the selected entry is disabled. -/// -/// The dropdown menu can be traversed by pressing the up or down key. During the -/// process, the corresponding item will be highlighted and the text field will be updated. -/// Disabled items will be skipped during traversal. -/// -/// The menu can be scrollable if not all items in the list are displayed at once. -/// -/// {@tool dartpad} -/// This sample shows how to display outlined [AFDropdownMenu] and filled [AFDropdownMenu]. -/// -/// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [MenuAnchor], which is a widget used to mark the "anchor" for a set of submenus. -/// The [AFDropdownMenu] uses a [TextField] as the "anchor". -/// * [TextField], which is a text input widget that uses an [InputDecoration]. -/// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [AFDropdownMenu] list. -class AFDropdownMenu extends StatefulWidget { - /// Creates a const [AFDropdownMenu]. - /// - /// The leading and trailing icons in the text field can be customized by using - /// [leadingIcon], [trailingIcon] and [selectedTrailingIcon] properties. They are - /// passed down to the [InputDecoration] properties, and will override values - /// in the [InputDecoration.prefixIcon] and [InputDecoration.suffixIcon]. - /// - /// Except leading and trailing icons, the text field can be configured by the - /// [InputDecorationTheme] property. The menu can be configured by the [menuStyle]. - const AFDropdownMenu({ - super.key, - this.enabled = true, - this.width, - this.menuHeight, - this.leadingIcon, - this.trailingIcon, - this.label, - this.hintText, - this.helperText, - this.errorText, - this.selectedTrailingIcon, - this.enableFilter = false, - this.enableSearch = true, - this.textStyle, - this.inputDecorationTheme, - this.menuStyle, - this.controller, - this.initialSelection, - this.onSelected, - this.requestFocusOnTap, - this.expandedInsets, - this.searchCallback, - this.selectOptionCompare, - required this.dropdownMenuEntries, - }); - - /// Determine if the [AFDropdownMenu] is enabled. - /// - /// Defaults to true. - final bool enabled; - - /// Determine the width of the [AFDropdownMenu]. - /// - /// If this is null, the width of the [AFDropdownMenu] will be the same as the width of the widest - /// menu item plus the width of the leading/trailing icon. - final double? width; - - /// Determine the height of the menu. - /// - /// If this is null, the menu will display as many items as possible on the screen. - final double? menuHeight; - - /// An optional Icon at the front of the text input field. - /// - /// Defaults to null. If this is not null, the menu items will have extra paddings to be aligned - /// with the text in the text field. - final Widget? leadingIcon; - - /// An optional icon at the end of the text field. - /// - /// Defaults to an [Icon] with [Icons.arrow_drop_down]. - final Widget? trailingIcon; - - /// Optional widget that describes the input field. - /// - /// When the input field is empty and unfocused, the label is displayed on - /// top of the input field (i.e., at the same location on the screen where - /// text may be entered in the input field). When the input field receives - /// focus (or if the field is non-empty), the label moves above, either - /// vertically adjacent to, or to the center of the input field. - /// - /// Defaults to null. - final Widget? label; - - /// Text that suggests what sort of input the field accepts. - /// - /// Defaults to null; - final String? hintText; - - /// Text that provides context about the [AFDropdownMenu]'s value, such - /// as how the value will be used. - /// - /// If non-null, the text is displayed below the input field, in - /// the same location as [errorText]. If a non-null [errorText] value is - /// specified then the helper text is not shown. - /// - /// Defaults to null; - /// - /// See also: - /// - /// * [InputDecoration.helperText], which is the text that provides context about the [InputDecorator.child]'s value. - final String? helperText; - - /// Text that appears below the input field and the border to show the error message. - /// - /// If non-null, the border's color animates to red and the [helperText] is not shown. - /// - /// Defaults to null; - /// - /// See also: - /// - /// * [InputDecoration.errorText], which is the text that appears below the [InputDecorator.child] and the border. - final String? errorText; - - /// An optional icon at the end of the text field to indicate that the text - /// field is pressed. - /// - /// Defaults to an [Icon] with [Icons.arrow_drop_up]. - final Widget? selectedTrailingIcon; - - /// Determine if the menu list can be filtered by the text input. - /// - /// Defaults to false. - final bool enableFilter; - - /// Determine if the first item that matches the text input can be highlighted. - /// - /// Defaults to true as the search function could be commonly used. - final bool enableSearch; - - /// The text style for the [TextField] of the [AFDropdownMenu]; - /// - /// Defaults to the overall theme's [TextTheme.bodyLarge] - /// if the dropdown menu theme's value is null. - final TextStyle? textStyle; - - /// Defines the default appearance of [InputDecoration] to show around the text field. - /// - /// By default, shows a outlined text field. - final InputDecorationTheme? inputDecorationTheme; - - /// The [MenuStyle] that defines the visual attributes of the menu. - /// - /// The default width of the menu is set to the width of the text field. - final MenuStyle? menuStyle; - - /// Controls the text being edited or selected in the menu. - /// - /// If null, this widget will create its own [TextEditingController]. - final TextEditingController? controller; - - /// The value used to for an initial selection. - /// - /// Defaults to null. - final T? initialSelection; - - /// The callback is called when a selection is made. - /// - /// Defaults to null. If null, only the text field is updated. - final ValueChanged? onSelected; - - /// Determine if the dropdown button requests focus and the on-screen virtual - /// keyboard is shown in response to a touch event. - /// - /// By default, on mobile platforms, tapping on the text field and opening - /// the menu will not cause a focus request and the virtual keyboard will not - /// appear. The default behavior for desktop platforms is for the dropdown to - /// take the focus. - /// - /// Defaults to null. Setting this field to true or false, rather than allowing - /// the implementation to choose based on the platform, can be useful for - /// applications that want to override the default behavior. - final bool? requestFocusOnTap; - - /// Descriptions of the menu items in the [AFDropdownMenu]. - /// - /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry] - /// is provided. If this is an empty list, the menu will be empty and only - /// contain space for padding. - final List> dropdownMenuEntries; - - /// Defines the menu text field's width to be equal to its parent's width - /// plus the horizontal width of the specified insets. - /// - /// If this property is null, the width of the text field will be determined - /// by the width of menu items or [AFDropdownMenu.width]. If this property is not null, - /// the text field's width will match the parent's width plus the specified insets. - /// If the value of this property is [EdgeInsets.zero], the width of the text field will be the same - /// as its parent's width. - /// - /// The [expandedInsets]' top and bottom are ignored, only its left and right - /// properties are used. - /// - /// Defaults to null. - final EdgeInsets? expandedInsets; - - /// When [AFDropdownMenu.enableSearch] is true, this callback is used to compute - /// the index of the search result to be highlighted. - /// - /// {@tool snippet} - /// - /// In this example the `searchCallback` returns the index of the search result - /// that exactly matches the query. - /// - /// ```dart - /// DropdownMenu( - /// searchCallback: (List> entries, String query) { - /// if (query.isEmpty) { - /// return null; - /// } - /// final int index = entries.indexWhere((DropdownMenuEntry entry) => entry.label == query); - /// - /// return index != -1 ? index : null; - /// }, - /// dropdownMenuEntries: const >[], - /// ) - /// ``` - /// {@end-tool} - /// - /// Defaults to null. If this is null and [AFDropdownMenu.enableSearch] is true, - /// the default function will return the index of the first matching result - /// 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(); -} - -class _AFDropdownMenuState extends State> { - final GlobalKey _anchorKey = GlobalKey(); - final GlobalKey _leadingKey = GlobalKey(); - late List buttonItemKeys; - final MenuController _controller = MenuController(); - late bool _enableFilter; - late List> filteredEntries; - List? _initialMenu; - int? currentHighlight; - double? leadingPadding; - bool _menuHasEnabledItem = false; - TextEditingController? _localTextEditingController; - TextEditingController get _textEditingController { - return widget.controller ?? - (_localTextEditingController ??= TextEditingController()); - } - - @override - void initState() { - super.initState(); - _enableFilter = widget.enableFilter; - filteredEntries = widget.dropdownMenuEntries; - buttonItemKeys = List.generate( - filteredEntries.length, - (int index) => GlobalKey(), - ); - _menuHasEnabledItem = - 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; - } - }, - ); - if (index != -1) { - _textEditingController.value = TextEditingValue( - text: filteredEntries[index].label, - selection: TextSelection.collapsed( - offset: filteredEntries[index].label.length, - ), - ); - } - refreshLeadingPadding(); - } - - @override - void dispose() { - _localTextEditingController?.dispose(); - _localTextEditingController = null; - super.dispose(); - } - - @override - void didUpdateWidget(AFDropdownMenu oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - if (widget.controller != null) { - _localTextEditingController?.dispose(); - _localTextEditingController = null; - } - } - if (oldWidget.enableSearch != widget.enableSearch) { - if (!widget.enableSearch) { - currentHighlight = null; - } - } - if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) { - currentHighlight = null; - filteredEntries = widget.dropdownMenuEntries; - buttonItemKeys = List.generate( - filteredEntries.length, - (int index) => GlobalKey(), - ); - _menuHasEnabledItem = - filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); - } - if (oldWidget.leadingIcon != widget.leadingIcon) { - refreshLeadingPadding(); - } - if (oldWidget.initialSelection != widget.initialSelection) { - final int index = filteredEntries.indexWhere( - (DropdownMenuEntry entry) => entry.value == widget.initialSelection, - ); - if (index != -1) { - _textEditingController.value = TextEditingValue( - text: filteredEntries[index].label, - selection: TextSelection.collapsed( - offset: filteredEntries[index].label.length, - ), - ); - } - } - } - - bool canRequestFocus() { - if (widget.requestFocusOnTap != null) { - return widget.requestFocusOnTap!; - } - - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.android: - case TargetPlatform.fuchsia: - return false; - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - return true; - } - } - - void refreshLeadingPadding() { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - setState(() { - leadingPadding = getWidth(_leadingKey); - }); - }, - debugLabel: 'DropdownMenu.refreshLeadingPadding', - ); - } - - // 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', - // ); - } - - double? getWidth(GlobalKey key) { - final BuildContext? context = key.currentContext; - if (context != null) { - final RenderBox box = context.findRenderObject()! as RenderBox; - return box.hasSize ? box.size.width : null; - } - return null; - } - - List> filter( - List> entries, - TextEditingController textEditingController, - ) { - final String filterText = textEditingController.text.toLowerCase(); - return entries - .where( - (DropdownMenuEntry entry) => - entry.label.toLowerCase().contains(filterText), - ) - .toList(); - } - - int? search( - List> entries, - TextEditingController textEditingController, - ) { - final String searchText = textEditingController.value.text.toLowerCase(); - if (searchText.isEmpty) { - return null; - } - final int index = entries.indexWhere( - (DropdownMenuEntry entry) => - entry.label.toLowerCase().contains(searchText), - ); - - return index != -1 ? index : null; - } - - List _buildButtons( - List> filteredEntries, - TextDirection textDirection, { - int? focusedIndex, - bool enableScrollToHighlight = true, - }) { - final List result = []; - for (int i = 0; i < filteredEntries.length; i++) { - final DropdownMenuEntry entry = filteredEntries[i]; - - // By default, when the text field has a leading icon but a menu entry doesn't - // have one, the label of the entry should have extra padding to be aligned - // with the text in the text input field. When both the text field and the - // menu entry have leading icons, the menu entry should remove the extra - // paddings so its leading icon will be aligned with the leading icon of - // the text field. - final double padding = entry.leadingIcon == null - ? (leadingPadding ?? _kDefaultHorizontalPadding) - : _kDefaultHorizontalPadding; - final ButtonStyle defaultStyle; - switch (textDirection) { - case TextDirection.rtl: - defaultStyle = MenuItemButton.styleFrom( - padding: EdgeInsets.only( - left: _kDefaultHorizontalPadding, - right: padding, - ), - ); - case TextDirection.ltr: - defaultStyle = MenuItemButton.styleFrom( - padding: EdgeInsets.only( - left: padding, - right: _kDefaultHorizontalPadding, - ), - ); - } - - ButtonStyle effectiveStyle = entry.style ?? defaultStyle; - final Color focusedBackgroundColor = effectiveStyle.foregroundColor - ?.resolve({WidgetState.focused}) ?? - Theme.of(context).colorScheme.onSurface; - - Widget label = entry.labelWidget ?? Text(entry.label); - if (widget.width != null) { - final double horizontalPadding = padding + _kDefaultHorizontalPadding; - label = ConstrainedBox( - constraints: - BoxConstraints(maxWidth: widget.width! - horizontalPadding), - child: label, - ); - } - - // 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). - effectiveStyle = entry.enabled && i == focusedIndex - ? effectiveStyle.copyWith( - backgroundColor: WidgetStatePropertyAll( - focusedBackgroundColor.withValues(alpha: 0.12), - ), - ) - : effectiveStyle; - - final Widget menuItemButton = Padding( - padding: const EdgeInsets.only(bottom: 6), - child: MenuItemButton( - key: enableScrollToHighlight ? buttonItemKeys[i] : null, - style: effectiveStyle, - leadingIcon: entry.leadingIcon, - trailingIcon: entry.trailingIcon, - onPressed: entry.enabled - ? () { - _textEditingController.value = TextEditingValue( - text: entry.label, - selection: - TextSelection.collapsed(offset: entry.label.length), - ); - currentHighlight = widget.enableSearch ? i : null; - widget.onSelected?.call(entry.value); - } - : null, - requestFocusOnHover: false, - child: label, - ), - ); - result.add(menuItemButton); - } - - return result; - } - - void handleUpKeyInvoke(_) { - setState(() { - if (!_menuHasEnabledItem || !_controller.isOpen) { - return; - } - _enableFilter = false; - currentHighlight ??= 0; - currentHighlight = (currentHighlight! - 1) % filteredEntries.length; - while (!filteredEntries[currentHighlight!].enabled) { - currentHighlight = (currentHighlight! - 1) % filteredEntries.length; - } - final String currentLabel = filteredEntries[currentHighlight!].label; - _textEditingController.value = TextEditingValue( - text: currentLabel, - selection: TextSelection.collapsed(offset: currentLabel.length), - ); - }); - } - - void handleDownKeyInvoke(_) { - setState(() { - if (!_menuHasEnabledItem || !_controller.isOpen) { - return; - } - _enableFilter = false; - currentHighlight ??= -1; - currentHighlight = (currentHighlight! + 1) % filteredEntries.length; - while (!filteredEntries[currentHighlight!].enabled) { - currentHighlight = (currentHighlight! + 1) % filteredEntries.length; - } - final String currentLabel = filteredEntries[currentHighlight!].label; - _textEditingController.value = TextEditingValue( - text: currentLabel, - selection: TextSelection.collapsed(offset: currentLabel.length), - ); - }); - } - - void handlePressed(MenuController controller) { - if (controller.isOpen) { - currentHighlight = null; - controller.close(); - } else { - // close to open - if (_textEditingController.text.isNotEmpty) { - _enableFilter = false; - } - controller.open(); - } - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final TextDirection textDirection = Directionality.of(context); - _initialMenu ??= _buildButtons( - widget.dropdownMenuEntries, - textDirection, - enableScrollToHighlight: false, - ); - final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); - final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); - - if (_enableFilter) { - filteredEntries = - filter(widget.dropdownMenuEntries, _textEditingController); - } - - if (widget.enableSearch) { - if (widget.searchCallback != null) { - currentHighlight = widget.searchCallback! - .call(filteredEntries, _textEditingController.text); - } else { - currentHighlight = search(filteredEntries, _textEditingController); - } - if (currentHighlight != null) { - scrollToHighlight(); - } - } - - final List menu = _buildButtons( - filteredEntries, - textDirection, - focusedIndex: currentHighlight, - ); - - final TextStyle? effectiveTextStyle = - widget.textStyle ?? theme.textStyle ?? defaults.textStyle; - - MenuStyle? effectiveMenuStyle = - widget.menuStyle ?? theme.menuStyle ?? defaults.menuStyle!; - - final double? anchorWidth = getWidth(_anchorKey); - if (widget.width != null) { - effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: WidgetStatePropertyAll(Size(widget.width!, 0.0)), - ); - } else if (anchorWidth != null) { - effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: WidgetStatePropertyAll(Size(anchorWidth, 0.0)), - ); - } - - if (widget.menuHeight != null) { - effectiveMenuStyle = effectiveMenuStyle.copyWith( - maximumSize: WidgetStatePropertyAll( - Size(double.infinity, widget.menuHeight!), - ), - ); - } - final InputDecorationTheme effectiveInputDecorationTheme = - widget.inputDecorationTheme ?? - theme.inputDecorationTheme ?? - defaults.inputDecorationTheme!; - - final MouseCursor effectiveMouseCursor = - canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click; - - Widget menuAnchor = MenuAnchor( - style: effectiveMenuStyle, - controller: _controller, - menuChildren: menu, - crossAxisUnconstrained: false, - builder: ( - BuildContext context, - MenuController controller, - Widget? child, - ) { - assert(_initialMenu != null); - final Widget trailingButton = Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - splashRadius: 1, - isSelected: controller.isOpen, - icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), - selectedIcon: - widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), - onPressed: () { - handlePressed(controller); - }, - ), - ); - - final Widget leadingButton = Padding( - padding: const EdgeInsets.all(8.0), - child: widget.leadingIcon ?? const SizedBox(), - ); - - final Widget textField = TextField( - key: _anchorKey, - mouseCursor: effectiveMouseCursor, - canRequestFocus: canRequestFocus(), - enableInteractiveSelection: canRequestFocus(), - textAlignVertical: TextAlignVertical.center, - style: effectiveTextStyle, - controller: _textEditingController, - onEditingComplete: () { - if (currentHighlight != null) { - final DropdownMenuEntry entry = - filteredEntries[currentHighlight!]; - if (entry.enabled) { - _textEditingController.value = TextEditingValue( - text: entry.label, - selection: - TextSelection.collapsed(offset: entry.label.length), - ); - widget.onSelected?.call(entry.value); - } - } else { - widget.onSelected?.call(null); - } - if (!widget.enableSearch) { - currentHighlight = null; - } - controller.close(); - }, - onTap: () { - handlePressed(controller); - }, - onChanged: (String text) { - controller.open(); - setState(() { - filteredEntries = widget.dropdownMenuEntries; - _enableFilter = widget.enableFilter; - }); - }, - decoration: InputDecoration( - enabled: widget.enabled, - label: widget.label, - hintText: widget.hintText, - helperText: widget.helperText, - errorText: widget.errorText, - prefixIcon: widget.leadingIcon != null - ? Container(key: _leadingKey, child: widget.leadingIcon) - : null, - suffixIcon: trailingButton, - ).applyDefaults(effectiveInputDecorationTheme), - ); - - if (widget.expandedInsets != null) { - // If [expandedInsets] is not null, the width of the text field should depend - // on its parent width. So we don't need to use `_DropdownMenuBody` to - // calculate the children's width. - return textField; - } - - return _DropdownMenuBody( - width: widget.width, - children: [ - textField, - for (final Widget item in _initialMenu!) item, - trailingButton, - leadingButton, - ], - ); - }, - ); - - if (widget.expandedInsets != null) { - menuAnchor = Container( - alignment: AlignmentDirectional.topStart, - padding: widget.expandedInsets?.copyWith(top: 0.0, bottom: 0.0), - child: menuAnchor, - ); - } - - return Shortcuts( - shortcuts: _kMenuTraversalShortcuts, - child: Actions( - actions: >{ - _ArrowUpIntent: CallbackAction<_ArrowUpIntent>( - onInvoke: handleUpKeyInvoke, - ), - _ArrowDownIntent: CallbackAction<_ArrowDownIntent>( - onInvoke: handleDownKeyInvoke, - ), - }, - child: menuAnchor, - ), - ); - } -} - -class _ArrowUpIntent extends Intent { - const _ArrowUpIntent(); -} - -class _ArrowDownIntent extends Intent { - const _ArrowDownIntent(); -} - -class _DropdownMenuBody extends MultiChildRenderObjectWidget { - const _DropdownMenuBody({ - super.children, - this.width, - }); - - final double? width; - - @override - _RenderDropdownMenuBody createRenderObject(BuildContext context) { - return _RenderDropdownMenuBody( - width: width, - ); - } - - @override - void updateRenderObject( - BuildContext context, - _RenderDropdownMenuBody renderObject, - ) { - renderObject.width = width; - } -} - -class _DropdownMenuBodyParentData extends ContainerBoxParentData {} - -class _RenderDropdownMenuBody extends RenderBox - with - ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { - _RenderDropdownMenuBody({ - double? width, - }) : _width = width; - - double? get width => _width; - double? _width; - set width(double? value) { - if (_width == value) { - return; - } - _width = value; - markNeedsLayout(); - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! _DropdownMenuBodyParentData) { - child.parentData = _DropdownMenuBodyParentData(); - } - } - - @override - void performLayout() { - final BoxConstraints constraints = this.constraints; - double maxWidth = 0.0; - double? maxHeight; - RenderBox? child = firstChild; - - final BoxConstraints innerConstraints = BoxConstraints( - maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth), - maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight), - ); - while (child != null) { - if (child == firstChild) { - child.layout(innerConstraints, parentUsesSize: true); - maxHeight ??= child.size.height; - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - continue; - } - child.layout(innerConstraints, parentUsesSize: true); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - childParentData.offset = Offset.zero; - maxWidth = math.max(maxWidth, child.size.width); - maxHeight ??= child.size.height; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - } - - assert(maxHeight != null); - maxWidth = math.max(_kMinimumWidth, maxWidth); - size = constraints.constrain(Size(width ?? maxWidth, maxHeight!)); - } - - @override - void paint(PaintingContext context, Offset offset) { - final RenderBox? child = firstChild; - if (child != null) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - context.paintChild(child, offset + childParentData.offset); - } - } - - @override - Size computeDryLayout(BoxConstraints constraints) { - final BoxConstraints constraints = this.constraints; - double maxWidth = 0.0; - double? maxHeight; - RenderBox? child = firstChild; - final BoxConstraints innerConstraints = BoxConstraints( - maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth), - maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight), - ); - - while (child != null) { - if (child == firstChild) { - final Size childSize = child.getDryLayout(innerConstraints); - maxHeight ??= childSize.height; - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - continue; - } - final Size childSize = child.getDryLayout(innerConstraints); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - childParentData.offset = Offset.zero; - maxWidth = math.max(maxWidth, childSize.width); - maxHeight ??= childSize.height; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - } - - assert(maxHeight != null); - maxWidth = math.max(_kMinimumWidth, maxWidth); - return constraints.constrain(Size(width ?? maxWidth, maxHeight!)); - } - - @override - double computeMinIntrinsicWidth(double height) { - RenderBox? child = firstChild; - double width = 0; - while (child != null) { - if (child == firstChild) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - continue; - } - final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); - if (child == lastChild) { - width += maxIntrinsicWidth; - } - if (child == childBefore(lastChild!)) { - width += maxIntrinsicWidth; - } - width = math.max(width, maxIntrinsicWidth); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - } - - return math.max(width, _kMinimumWidth); - } - - @override - double computeMaxIntrinsicWidth(double height) { - RenderBox? child = firstChild; - double width = 0; - while (child != null) { - if (child == firstChild) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - continue; - } - final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height); - // Add the width of leading Icon. - if (child == lastChild) { - width += maxIntrinsicWidth; - } - // Add the width of trailing Icon. - if (child == childBefore(lastChild!)) { - width += maxIntrinsicWidth; - } - width = math.max(width, maxIntrinsicWidth); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - } - - return math.max(width, _kMinimumWidth); - } - - @override - double computeMinIntrinsicHeight(double height) { - final RenderBox? child = firstChild; - double width = 0; - if (child != null) { - width = math.max(width, child.getMinIntrinsicHeight(height)); - } - return width; - } - - @override - double computeMaxIntrinsicHeight(double height) { - final RenderBox? child = firstChild; - double width = 0; - if (child != null) { - width = math.max(width, child.getMaxIntrinsicHeight(height)); - } - return width; - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - final RenderBox? child = firstChild; - if (child != null) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - final bool isHit = result.addWithPaintOffset( - offset: childParentData.offset, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(transformed == position - childParentData.offset); - return child.hitTest(result, position: transformed); - }, - ); - if (isHit) { - return true; - } - } - return false; - } -} - -// Hand coded defaults. These will be updated once we have tokens/spec. -class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { - _DropdownMenuDefaultsM3(this.context); - - final BuildContext context; - late final ThemeData _theme = Theme.of(context); - - @override - TextStyle? get textStyle => _theme.textTheme.bodyLarge; - - @override - MenuStyle get menuStyle { - return const MenuStyle( - minimumSize: WidgetStatePropertyAll(Size(_kMinimumWidth, 0.0)), - maximumSize: WidgetStatePropertyAll(Size.infinite), - visualDensity: VisualDensity.standard, - ); - } - - @override - InputDecorationTheme get inputDecorationTheme { - return const InputDecorationTheme(border: OutlineInputBorder()); - } -} diff --git a/frontend/appflowy_flutter/lib/main.dart b/frontend/appflowy_flutter/lib/main.dart index 9117acfd1b..4da98d2eae 100644 --- a/frontend/appflowy_flutter/lib/main.dart +++ b/frontend/appflowy_flutter/lib/main.dart @@ -1,11 +1,13 @@ -import 'package:scaled_app/scaled_app.dart'; +import 'package:appflowy/startup/entry_point.dart'; +import 'package:flutter/material.dart'; import 'startup/startup.dart'; Future main() async { - ScaledWidgetsFlutterBinding.ensureInitialized( - scaleFactor: (_) => 1.0, - ); + WidgetsFlutterBinding.ensureInitialized(); - await runAppFlowy(); + await FlowyRunner.run( + FlowyApp(), + integrationEnv(), + ); } diff --git a/frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart deleted file mode 100644 index 50426b5761..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/application/view/view_ext.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'; - -part 'mobile_view_page_bloc.freezed.dart'; - -class MobileViewPageBloc - extends Bloc { - MobileViewPageBloc({ - required this.viewId, - }) : _viewListener = ViewListener(viewId: viewId), - super(MobileViewPageState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - _registerListeners(); - - final userProfilePB = - await UserBackendService.getCurrentUserProfile() - .fold((s) => s, (f) => null); - final result = await ViewBackendService.getView(viewId); - final isImmersiveMode = - _isImmersiveMode(result.fold((s) => s, (f) => null)); - emit( - state.copyWith( - isLoading: false, - result: result, - isImmersiveMode: isImmersiveMode, - userProfilePB: userProfilePB, - ), - ); - }, - updateImmersionMode: (isImmersiveMode) { - emit( - state.copyWith( - isImmersiveMode: isImmersiveMode, - ), - ); - }, - ); - }, - ); - } - - final String viewId; - final ViewListener _viewListener; - - @override - Future close() { - _viewListener.stop(); - return super.close(); - } - - void _registerListeners() { - _viewListener.start( - onViewUpdated: (view) { - final isImmersiveMode = _isImmersiveMode(view); - add(MobileViewPageEvent.updateImmersionMode(isImmersiveMode)); - }, - ); - } - - // only the document page supports immersive mode (version 0.5.6) - bool _isImmersiveMode(ViewPB? view) { - if (view == null) { - return false; - } - - final cover = view.cover; - if (cover == null || cover.type == PageStyleCoverImageType.none) { - return false; - } else if (view.layout == ViewLayoutPB.Document && !cover.isPresets) { - // only support immersive mode for document layout - return true; - } - - return false; - } -} - -@freezed -class MobileViewPageEvent with _$MobileViewPageEvent { - const factory MobileViewPageEvent.initial() = Initial; - const factory MobileViewPageEvent.updateImmersionMode(bool isImmersiveMode) = - UpdateImmersionMode; -} - -@freezed -class MobileViewPageState with _$MobileViewPageState { - const factory MobileViewPageState({ - @Default(true) bool isLoading, - @Default(null) FlowyResult? result, - @Default(false) bool isImmersiveMode, - @Default(null) UserProfilePB? userProfilePB, - }) = _MobileViewPageState; - - factory MobileViewPageState.initial() => const MobileViewPageState(); -} diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart deleted file mode 100644 index aa02495a49..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.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 { - // 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, - ).toString(); - await push(uri); - } -} - -extension on ViewPB { - String get routeName { - switch (layout) { - case ViewLayoutPB.Document: - return MobileDocumentScreen.routeName; - case ViewLayoutPB.Grid: - return MobileGridScreen.routeName; - case ViewLayoutPB.Calendar: - return MobileCalendarScreen.routeName; - case ViewLayoutPB.Board: - return MobileBoardScreen.routeName; - case ViewLayoutPB.Chat: - return MobileChatScreen.routeName; - - default: - throw UnimplementedError('routeName for $this is not implemented'); - } - } - - Map queryParameters([Map? arguments]) { - switch (layout) { - case ViewLayoutPB.Document: - return { - MobileDocumentScreen.viewId: id, - MobileDocumentScreen.viewTitle: name, - }; - case ViewLayoutPB.Grid: - return { - MobileGridScreen.viewId: id, - MobileGridScreen.viewTitle: name, - MobileGridScreen.viewArgs: jsonEncode(arguments), - }; - case ViewLayoutPB.Calendar: - return { - MobileCalendarScreen.viewId: id, - MobileCalendarScreen.viewTitle: name, - }; - case ViewLayoutPB.Board: - return { - MobileBoardScreen.viewId: id, - MobileBoardScreen.viewTitle: name, - }; - case ViewLayoutPB.Chat: - return { - MobileChatScreen.viewId: id, - MobileChatScreen.viewTitle: name, - }; - default: - throw UnimplementedError( - 'queryParameters for $this is not implemented', - ); - } - } -} 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 deleted file mode 100644 index 650fbf1d85..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart +++ /dev/null @@ -1,456 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; - -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:collection/collection.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'document_page_style_bloc.freezed.dart'; - -class DocumentPageStyleBloc - extends Bloc { - DocumentPageStyleBloc({ - required this.view, - }) : super(DocumentPageStyleState.initial()) { - on( - (event, emit) async { - 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 fontLayout = _getSelectedFontLayout(layoutObject); - final lineHeightLayout = _getSelectedLineHeightLayout( - layoutObject, - ); - final fontFamily = _getSelectedFontFamily(layoutObject); - final cover = _getSelectedCover(layoutObject); - final coverType = cover.$1; - final coverValue = cover.$2; - emit( - state.copyWith( - fontLayout: fontLayout, - fontFamily: fontFamily, - lineHeightLayout: lineHeightLayout, - coverImage: PageStyleCover( - type: coverType, - value: coverValue, - ), - iconPadding: calculateIconPadding( - fontLayout, - lineHeightLayout, - ), - ), - ); - } catch (e) { - Log.error('Failed to decode layout object: $e'); - } - }, - updateFont: (fontLayout) async { - emit( - state.copyWith( - fontLayout: fontLayout, - iconPadding: calculateIconPadding( - fontLayout, - state.lineHeightLayout, - ), - ), - ); - - unawaited(updateLayoutObject()); - }, - updateLineHeight: (lineHeightLayout) async { - emit( - state.copyWith( - lineHeightLayout: lineHeightLayout, - iconPadding: calculateIconPadding( - state.fontLayout, - lineHeightLayout, - ), - ), - ); - - unawaited(updateLayoutObject()); - }, - updateFontFamily: (fontFamily) async { - emit( - state.copyWith( - fontFamily: fontFamily, - ), - ); - - unawaited(updateLayoutObject()); - }, - updateCoverImage: (coverImage) async { - emit( - state.copyWith( - coverImage: coverImage, - ), - ); - - unawaited(updateLayoutObject()); - }, - ); - }, - ); - } - - final ViewPB view; - final ViewBackendService viewBackendService = ViewBackendService(); - - Future updateLayoutObject() async { - final layoutObject = decodeLayoutObject(); - if (layoutObject != null) { - await ViewBackendService.updateView( - viewId: view.id, - extra: layoutObject, - ); - } - } - - String? decodeLayoutObject() { - Map oldValue = {}; - try { - final extra = view.extra; - oldValue = jsonDecode(extra); - } catch (e) { - Log.error('Failed to decode layout object: $e'); - } - final newValue = { - ViewExtKeys.fontLayoutKey: state.fontLayout.toString(), - ViewExtKeys.lineHeightLayoutKey: state.lineHeightLayout.toString(), - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: state.coverImage.type.toString(), - ViewExtKeys.coverValueKey: state.coverImage.value, - }, - ViewExtKeys.fontKey: state.fontFamily, - }; - final merged = mergeMaps(oldValue, newValue); - return jsonEncode(merged); - } - - // because the line height can not be calculated accurately, - // we need to adjust the icon padding manually. - double calculateIconPadding( - PageStyleFontLayout fontLayout, - PageStyleLineHeightLayout lineHeightLayout, - ) { - double padding = switch (fontLayout) { - PageStyleFontLayout.small => 1.0, - PageStyleFontLayout.normal => 1.0, - PageStyleFontLayout.large => 4.0, - }; - switch (lineHeightLayout) { - case PageStyleLineHeightLayout.small: - padding -= 1.0; - break; - case PageStyleLineHeightLayout.normal: - break; - case PageStyleLineHeightLayout.large: - padding += 3.0; - break; - } - 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(); - return PageStyleFontLayout.values.firstWhere( - (e) => e.toString() == fontLayout, - ); - } - - PageStyleLineHeightLayout _getSelectedLineHeightLayout(Map layoutObject) { - final lineHeightLayout = layoutObject[ViewExtKeys.lineHeightLayoutKey] ?? - PageStyleLineHeightLayout.normal.toString(); - return PageStyleLineHeightLayout.values.firstWhere( - (e) => e.toString() == lineHeightLayout, - ); - } - - String? _getSelectedFontFamily(Map layoutObject) { - return layoutObject[ViewExtKeys.fontKey]; - } - - (PageStyleCoverImageType, String colorValue) _getSelectedCover( - Map layoutObject, - ) { - final cover = layoutObject[ViewExtKeys.coverKey] ?? {}; - final coverType = cover[ViewExtKeys.coverTypeKey] ?? - PageStyleCoverImageType.none.toString(); - final coverValue = cover[ViewExtKeys.coverValueKey] ?? ''; - return ( - PageStyleCoverImageType.values.firstWhere( - (e) => e.toString() == coverType, - ), - coverValue, - ); - } -} - -@freezed -class DocumentPageStyleEvent with _$DocumentPageStyleEvent { - const factory DocumentPageStyleEvent.initial() = Initial; - const factory DocumentPageStyleEvent.updateFont( - PageStyleFontLayout fontLayout, - ) = UpdateFontSize; - const factory DocumentPageStyleEvent.updateLineHeight( - PageStyleLineHeightLayout lineHeightLayout, - ) = UpdateLineHeight; - const factory DocumentPageStyleEvent.updateFontFamily( - String? fontFamily, - ) = UpdateFontFamily; - const factory DocumentPageStyleEvent.updateCoverImage( - PageStyleCover coverImage, - ) = UpdateCoverImage; -} - -@freezed -class DocumentPageStyleState with _$DocumentPageStyleState { - const factory DocumentPageStyleState({ - @Default(PageStyleFontLayout.normal) PageStyleFontLayout fontLayout, - @Default(PageStyleLineHeightLayout.normal) - PageStyleLineHeightLayout lineHeightLayout, - // the default font family is null, which means the system font - @Default(null) String? fontFamily, - @Default(2.0) double iconPadding, - required PageStyleCover coverImage, - }) = _DocumentPageStyleState; - - factory DocumentPageStyleState.initial() => DocumentPageStyleState( - coverImage: PageStyleCover.none(), - ); -} - -enum PageStyleFontLayout { - small, - normal, - large; - - @override - String toString() { - switch (this) { - case PageStyleFontLayout.small: - return 'small'; - case PageStyleFontLayout.normal: - return 'normal'; - case PageStyleFontLayout.large: - return 'large'; - } - } - - static PageStyleFontLayout fromString(String value) { - return PageStyleFontLayout.values.firstWhereOrNull( - (e) => e.toString() == value, - ) ?? - PageStyleFontLayout.normal; - } - - double get fontSize { - switch (this) { - case PageStyleFontLayout.small: - return 14.0; - case PageStyleFontLayout.normal: - return 16.0; - case PageStyleFontLayout.large: - return 18.0; - } - } - - List get headingFontSizes { - switch (this) { - case PageStyleFontLayout.small: - return [22.0, 18.0, 16.0, 16.0, 16.0, 16.0]; - case PageStyleFontLayout.normal: - return [24.0, 20.0, 18.0, 18.0, 18.0, 18.0]; - case PageStyleFontLayout.large: - return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0]; - } - } - - double get factor { - switch (this) { - case PageStyleFontLayout.small: - return PageStyleFontLayout.small.fontSize / - PageStyleFontLayout.normal.fontSize; - case PageStyleFontLayout.normal: - return 1.0; - case PageStyleFontLayout.large: - return PageStyleFontLayout.large.fontSize / - PageStyleFontLayout.normal.fontSize; - } - } -} - -enum PageStyleLineHeightLayout { - small, - normal, - large; - - @override - String toString() { - switch (this) { - case PageStyleLineHeightLayout.small: - return 'small'; - case PageStyleLineHeightLayout.normal: - return 'normal'; - case PageStyleLineHeightLayout.large: - return 'large'; - } - } - - static PageStyleLineHeightLayout fromString(String value) { - return PageStyleLineHeightLayout.values.firstWhereOrNull( - (e) => e.toString() == value, - ) ?? - PageStyleLineHeightLayout.normal; - } - - double get lineHeight { - switch (this) { - case PageStyleLineHeightLayout.small: - return 1.4; - case PageStyleLineHeightLayout.normal: - return 1.5; - case PageStyleLineHeightLayout.large: - return 1.75; - } - } - - double get padding { - switch (this) { - case PageStyleLineHeightLayout.small: - return 6.0; - case PageStyleLineHeightLayout.normal: - return 8.0; - case PageStyleLineHeightLayout.large: - return 8.0; - } - } - - List get headingPaddings { - switch (this) { - case PageStyleLineHeightLayout.small: - return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0]; - case PageStyleLineHeightLayout.normal: - return [30.0, 24.0, 22.0, 22.0, 22.0, 22.0]; - case PageStyleLineHeightLayout.large: - return [34.0, 28.0, 26.0, 26.0, 26.0, 26.0]; - } - } -} - -// for the version above 0.5.5 -enum PageStyleCoverImageType { - none, - // normal color - pureColor, - // gradient color - gradientColor, - // built in images - builtInImage, - // custom images, uploaded by the user - customImage, - // local image - localImage, - // unsplash images - unsplashImage; - - @override - String toString() { - switch (this) { - case PageStyleCoverImageType.none: - return 'none'; - case PageStyleCoverImageType.pureColor: - return 'color'; - case PageStyleCoverImageType.gradientColor: - return 'gradient'; - case PageStyleCoverImageType.builtInImage: - return 'built_in'; - case PageStyleCoverImageType.customImage: - return 'custom'; - case PageStyleCoverImageType.localImage: - return 'local'; - case PageStyleCoverImageType.unsplashImage: - return 'unsplash'; - } - } - - static PageStyleCoverImageType fromString(String value) { - return PageStyleCoverImageType.values.firstWhereOrNull( - (e) => e.toString() == value, - ) ?? - PageStyleCoverImageType.none; - } - - static String builtInImagePath(String value) { - return 'assets/images/built_in_cover_images/m_cover_image_$value.png'; - } -} - -class PageStyleCover { - const PageStyleCover({ - required this.type, - required this.value, - }); - - factory PageStyleCover.none() => const PageStyleCover( - type: PageStyleCoverImageType.none, - value: '', - ); - - final PageStyleCoverImageType type; - - // there're 4 types of values: - // 1. pure color: enum value - // 2. gradient color: enum value - // 3. built-in image: the image name, read from the assets - // 4. custom image or unsplash image: the image url - final String value; - - bool get isPresets => isPureColor || isGradient || isBuiltInImage; - bool get isPhoto => isCustomImage || isLocalImage; - - bool get isNone => type == PageStyleCoverImageType.none; - bool get isPureColor => type == PageStyleCoverImageType.pureColor; - bool get isGradient => type == PageStyleCoverImageType.gradientColor; - bool get isBuiltInImage => type == PageStyleCoverImageType.builtInImage; - 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 deleted file mode 100644 index 2d89b3b388..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_listener.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'; -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 { - RecentViewBloc({ - required this.view, - }) : _documentListener = DocumentListener(id: view.id), - _viewListener = ViewListener(viewId: view.id), - super(RecentViewState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - _documentListener.start( - onDocEventUpdate: (docEvent) async { - if (state.coverTypeV2 != null) { - return; - } - final (coverType, coverValue) = await getCoverV1(); - add( - RecentViewEvent.updateCover( - coverType, - null, - coverValue, - ), - ); - }, - ); - _viewListener.start( - onViewUpdated: (view) { - add( - RecentViewEvent.updateNameOrIcon( - view.name, - view.icon.toEmojiIconData(), - ), - ); - - if (view.extra.isNotEmpty) { - final cover = view.cover; - add( - RecentViewEvent.updateCover( - CoverType.none, - cover?.type, - cover?.value, - ), - ); - } - }, - ); - - // only document supports the cover - if (view.layout != ViewLayoutPB.Document) { - emit( - state.copyWith( - name: view.name, - icon: view.icon.toEmojiIconData(), - ), - ); - } - - final cover = getCoverV2(); - - if (cover != null) { - emit( - state.copyWith( - name: view.name, - icon: view.icon.toEmojiIconData(), - coverTypeV2: cover.type, - coverValue: cover.value, - ), - ); - } else { - final (coverTypeV1, coverValue) = await getCoverV1(); - emit( - state.copyWith( - name: view.name, - icon: view.icon.toEmojiIconData(), - coverTypeV1: coverTypeV1, - coverValue: coverValue, - ), - ); - } - }, - updateNameOrIcon: (name, icon) { - emit( - state.copyWith( - name: name, - icon: icon, - ), - ); - }, - updateCover: (coverTypeV1, coverTypeV2, coverValue) { - emit( - state.copyWith( - coverTypeV1: coverTypeV1, - coverTypeV2: coverTypeV2, - coverValue: coverValue, - ), - ); - }, - ); - }, - ); - } - - final ViewPB view; - final DocumentListener _documentListener; - final ViewListener _viewListener; - - PageStyleCover? getCoverV2() { - return view.cover; - } - - // for the version under 0.5.5 - Future<(CoverType, String?)> getCoverV1() async { - return (CoverType.none, null); - } - - @override - Future close() async { - await _documentListener.stop(); - await _viewListener.stop(); - return super.close(); - } -} - -@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 - PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5 - String? coverValue, - ) = UpdateCover; - - const factory RecentViewEvent.updateNameOrIcon( - String name, - EmojiIconData icon, - ) = UpdateNameOrIcon; -} - -@freezed -class RecentViewState with _$RecentViewState { - const factory RecentViewState({ - required String name, - required EmojiIconData icon, - @Default(CoverType.none) CoverType coverTypeV1, - PageStyleCoverImageType? coverTypeV2, - @Default(null) String? coverValue, - }) = _RecentViewState; - - factory RecentViewState.initial() => - RecentViewState(name: '', icon: EmojiIconData.none()); -} 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 deleted file mode 100644 index 0527316860..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'user_profile_bloc.freezed.dart'; - -class UserProfileBloc extends Bloc { - UserProfileBloc() : super(const _Initial()) { - on((event, emit) async { - await event.when( - started: () async => _initialize(emit), - ); - }); - } - - Future _initialize(Emitter emit) async { - emit(const UserProfileState.loading()); - final latestOrFailure = - await FolderEventGetCurrentWorkspaceSetting().send(); - - final userOrFailure = await getIt().getUser(); - - final latest = latestOrFailure.fold( - (latestPB) => latestPB, - (error) => null, - ); - - final userProfile = userOrFailure.fold( - (userProfilePB) => userProfilePB, - (error) => null, - ); - - if (latest == null || userProfile == null) { - return emit(const UserProfileState.workspaceFailure()); - } - - emit( - UserProfileState.success( - workspaceSettings: latest, - userProfile: userProfile, - ), - ); - } -} - -@freezed -class UserProfileEvent with _$UserProfileEvent { - const factory UserProfileEvent.started() = _Started; -} - -@freezed -class UserProfileState with _$UserProfileState { - const factory UserProfileState.initial() = _Initial; - const factory UserProfileState.loading() = _Loading; - const factory UserProfileState.workspaceFailure() = _WorkspaceFailure; - const factory UserProfileState.success({ - required WorkspaceLatestPB 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 deleted file mode 100644 index 396ecd6bb8..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -enum FlowyAppBarLeadingType { - back, - close, - cancel; - - Widget getWidget(VoidCallback? onTap) { - switch (this) { - case FlowyAppBarLeadingType.back: - return AppBarImmersiveBackButton(onTap: onTap); - case FlowyAppBarLeadingType.close: - return AppBarCloseButton(onTap: onTap); - case FlowyAppBarLeadingType.cancel: - return AppBarCancelButton(onTap: onTap); - } - } - - double? get width { - switch (this) { - case FlowyAppBarLeadingType.back: - return 40.0; - case FlowyAppBarLeadingType.close: - return 40.0; - case FlowyAppBarLeadingType.cancel: - return 120; - } - } -} - -class FlowyAppBar extends AppBar { - FlowyAppBar({ - super.key, - super.actions, - Widget? title, - String? titleText, - FlowyAppBarLeadingType leadingType = FlowyAppBarLeadingType.back, - double? leadingWidth, - Widget? leading, - super.centerTitle, - VoidCallback? onTapLeading, - bool showDivider = true, - super.backgroundColor, - }) : super( - title: title ?? - FlowyText( - titleText ?? '', - fontSize: 15.0, - fontWeight: FontWeight.w500, - ), - titleSpacing: 0, - elevation: 0, - leading: leading ?? leadingType.getWidget(onTapLeading), - leadingWidth: leadingWidth ?? leadingType.width, - toolbarHeight: 44.0, - bottom: showDivider - ? const PreferredSize( - preferredSize: Size.fromHeight(0.5), - child: Divider( - height: 0.5, - ), - ) - : null, - ); -} 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 deleted file mode 100644 index 72142d446b..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class AppBarBackButton extends StatelessWidget { - const AppBarBackButton({ - super.key, - this.onTap, - this.padding, - }); - - final VoidCallback? onTap; - final EdgeInsetsGeometry? padding; - - @override - Widget build(BuildContext context) { - return AppBarButton( - onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(), - padding: padding, - child: const FlowySvg( - FlowySvgs.m_app_bar_back_s, - ), - ); - } -} - -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, - this.onTap, - }); - - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return AppBarButton( - onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(), - child: const FlowySvg( - FlowySvgs.m_app_bar_close_s, - ), - ); - } -} - -class AppBarCancelButton extends StatelessWidget { - const AppBarCancelButton({ - super.key, - this.onTap, - }); - - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return AppBarButton( - onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(), - child: FlowyText( - LocaleKeys.button_cancel.tr(), - overflow: TextOverflow.ellipsis, - ), - ); - } -} - -class AppBarDoneButton extends StatelessWidget { - const AppBarDoneButton({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AppBarButton( - onTap: (_) => onTap(), - padding: const EdgeInsets.all(12), - child: FlowyText( - LocaleKeys.button_done.tr(), - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500, - textAlign: TextAlign.right, - ), - ); - } -} - -class AppBarSaveButton extends StatelessWidget { - const AppBarSaveButton({ - super.key, - required this.onTap, - this.enable = true, - this.padding = const EdgeInsets.all(12), - }); - - final VoidCallback onTap; - final bool enable; - final EdgeInsetsGeometry padding; - - @override - Widget build(BuildContext context) { - return AppBarButton( - onTap: (_) { - if (enable) { - onTap(); - } - }, - padding: padding, - child: FlowyText( - LocaleKeys.button_save.tr(), - color: enable - ? Theme.of(context).colorScheme.primary - : Theme.of(context).disabledColor, - fontWeight: FontWeight.w500, - textAlign: TextAlign.right, - ), - ); - } -} - -class AppBarFilledDoneButton extends StatelessWidget { - const AppBarFilledDoneButton({super.key, required this.onTap}); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), - child: TextButton( - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - elevation: 0, - visualDensity: VisualDensity.compact, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - enableFeedback: true, - backgroundColor: Theme.of(context).primaryColor, - ), - onPressed: onTap, - child: FlowyText.medium( - LocaleKeys.button_done.tr(), - fontSize: 16, - color: Theme.of(context).colorScheme.onPrimary, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } -} - -class AppBarMoreButton extends StatelessWidget { - const AppBarMoreButton({ - super.key, - required this.onTap, - }); - - final void Function(BuildContext context) onTap; - - @override - Widget build(BuildContext context) { - return AppBarButton( - padding: const EdgeInsets.all(12), - onTap: onTap, - child: const FlowySvg(FlowySvgs.three_dots_s), - ); - } -} - -class AppBarButton extends StatelessWidget { - const AppBarButton({ - super.key, - required this.onTap, - required this.child, - this.padding, - }); - - final void Function(BuildContext context) onTap; - final Widget child; - final EdgeInsetsGeometry? padding; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => onTap(context), - child: Padding( - padding: padding ?? const EdgeInsets.all(12), - child: child, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/flowy_search_text_field.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/flowy_search_text_field.dart deleted file mode 100644 index 8233f8bff5..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/flowy_search_text_field.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class FlowySearchTextField extends StatelessWidget { - const FlowySearchTextField({ - super.key, - this.hintText, - this.controller, - this.onChanged, - this.onSubmitted, - }); - - final String? hintText; - final TextEditingController? controller; - final ValueChanged? onChanged; - final ValueChanged? onSubmitted; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 44.0, - child: CupertinoSearchTextField( - controller: controller, - onChanged: onChanged, - onSubmitted: onSubmitted, - placeholder: hintText, - prefixIcon: const FlowySvg(FlowySvgs.m_search_m), - prefixInsets: const EdgeInsets.only(left: 16.0, right: 2.0), - suffixIcon: const Icon(Icons.close), - suffixInsets: const EdgeInsets.only(right: 16.0), - placeholderStyle: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w400, - fontSize: 14.0, - ), - ), - ); - } -} 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 deleted file mode 100644 index 318b06394a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ /dev/null @@ -1,449 +0,0 @@ -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/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'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileViewPage extends StatefulWidget { - const MobileViewPage({ - super.key, - required this.id, - required this.viewLayout, - this.title, - this.arguments, - this.fixedTitle, - this.showMoreButton = true, - this.blockId, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], - }); - - /// view id - final String id; - 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(); -} - -class _MobileViewPageState extends State { - // used to determine if the user has scrolled down and show the app bar in immersive mode - ScrollNotificationObserverState? _scrollNotificationObserver; - - // 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(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => MobileViewPageBloc(viewId: widget.id) - ..add(const MobileViewPageEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final view = state.result?.fold((s) => s, (f) => null); - final body = _buildBody(context, state); - - if (view == null) { - return SizedBox.shrink(); - } - - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => - FavoriteBloc()..add(const FavoriteEvent.initial()), - ), - BlocProvider( - create: (_) => - ViewBloc(view: view)..add(const ViewEvent.initial()), - ), - BlocProvider.value( - value: getIt(), - ), - 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) { - final view = context.watch().state.view; - return _buildApp(context, view, body); - }, - ), - ); - }, - ), - ); - } - - Widget _buildApp( - BuildContext context, - ViewPB? view, - Widget child, - ) { - final isDocument = view?.layout.isDocumentView ?? false; - final title = _buildTitle(context, view); - final actions = _buildAppBarActions(context, view); - final appBar = isDocument - ? MobileViewPageImmersiveAppBar( - preferredSize: Size( - double.infinity, - AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight, - ), - title: title, - appBarOpacity: _appBarOpacity, - actions: actions, - view: view, - ) - : FlowyAppBar(title: title, actions: actions); - final body = isDocument - ? Builder( - builder: (context) { - _rebuildScrollNotificationObserver(context); - return child; - }, - ) - : SafeArea(child: child); - return Scaffold( - extendBodyBehindAppBar: isDocument, - appBar: appBar, - body: body, - ); - } - - Widget _buildBody(BuildContext context, MobileViewPageState state) { - if (state.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - final result = state.result; - if (result == null) { - return FlowyMobileStateContainer.error( - emoji: '😔', - title: LocaleKeys.error_weAreSorry.tr(), - description: LocaleKeys.error_loadingViewError.tr(), - errorMsg: '', - ); - } - - return result.fold( - (view) { - final plugin = view.plugin(arguments: widget.arguments ?? const {}) - ..init(); - 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) { - return FlowyMobileStateContainer.error( - emoji: '😔', - title: LocaleKeys.error_weAreSorry.tr(), - description: LocaleKeys.error_loadingViewError.tr(), - errorMsg: error.toString(), - ); - }, - ); - } - - // Document: - // - [ collaborators, sync_indicator, layout_button, more_button] - // Database: - // - [ sync_indicator, more_button] - List _buildAppBarActions(BuildContext context, ViewPB? view) { - if (view == null) { - return []; - } - - final isImmersiveMode = - context.read().state.isImmersiveMode; - final isLocked = - context.read()?.state.isLocked ?? false; - final actions = []; - - if (FeatureFlag.syncDocument.isOn) { - // only document supports displaying collaborators. - if (view.layout.isDocumentView) { - actions.addAll([ - DocumentCollaborators( - width: 60, - height: 44, - fontSize: 14, - padding: const EdgeInsets.symmetric(vertical: 8), - view: view, - ), - const HSpace(12.0), - ]); - } - } - - if (view.layout.isDocumentView && !isLocked) { - 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), - ]); - } - - 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), - ], - ), - ); - }, - ); - } - - 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(); - }, - ); - } - - void _rebuildScrollNotificationObserver(BuildContext context) { - _scrollNotificationObserver?.removeListener(_onScrollNotification); - _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); - _scrollNotificationObserver?.addListener(_onScrollNotification); - } - - // immersive mode related - // auto show or hide the app bar based on the scroll position - void _onScrollNotification(ScrollNotification notification) { - if (_scrollNotificationObserver == null) { - return; - } - - if (notification is ScrollUpdateNotification && - defaultScrollNotificationPredicate(notification)) { - final ScrollMetrics metrics = notification.metrics; - double height = MediaQuery.of(context).padding.top; - if (defaultTargetPlatform == TargetPlatform.android) { - height += AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight; - } - final progress = (metrics.pixels / height).clamp(0.0, 1.0); - // reduce the sensitivity of the app bar opacity change - if ((progress - _appBarOpacity.value).abs() >= 0.1 || - progress == 0 || - progress == 1.0) { - _appBarOpacity.value = progress; - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart deleted file mode 100644 index d27085b394..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flutter/material.dart'; - -class OptionColorList extends StatelessWidget { - const OptionColorList({ - super.key, - this.selectedColor, - required this.onSelectedColor, - }); - - final SelectOptionColorPB? selectedColor; - final void Function(SelectOptionColorPB color) onSelectedColor; - - @override - Widget build(BuildContext context) { - return GridView.count( - crossAxisCount: 6, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - children: SelectOptionColorPB.values.map( - (colorPB) { - final color = colorPB.toColor(context); - final isSelected = selectedColor?.value == colorPB.value; - return GestureDetector( - onTap: () => onSelectedColor(colorPB), - child: Container( - margin: const EdgeInsets.all( - 8.0, - ), - decoration: BoxDecoration( - color: color, - borderRadius: Corners.s12Border, - border: Border.all( - width: isSelected ? 2.0 : 1.0, - color: isSelected - ? const Color(0xff00C6F1) - : Theme.of(context).dividerColor, - ), - ), - alignment: Alignment.center, - child: isSelected - ? const FlowySvg( - FlowySvgs.m_blue_check_s, - size: Size.square(28.0), - blendMode: null, - ) - : null, - ), - ); - }, - ).toList(), - ); - } -} 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 deleted file mode 100644 index e0c3140ea9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart +++ /dev/null @@ -1,162 +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 TypeOptionMenuItemValue { - const TypeOptionMenuItemValue({ - required this.value, - required this.icon, - 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; -} - -class TypeOptionMenu extends StatelessWidget { - const TypeOptionMenu({ - super.key, - required this.values, - this.width = 98, - this.iconWidth = 72, - this.scaleFactor = 1.0, - this.maxAxisSpacing = 18, - this.crossAxisCount = 3, - }); - - final List> values; - - final double iconWidth; - final double width; - final double scaleFactor; - final double maxAxisSpacing; - final int crossAxisCount; - - @override - Widget build(BuildContext context) { - return TypeOptionGridView( - crossAxisCount: crossAxisCount, - mainAxisSpacing: maxAxisSpacing * scaleFactor, - itemWidth: width * scaleFactor, - children: values - .map( - (value) => TypeOptionMenuItem( - value: value, - width: width, - iconWidth: iconWidth, - scaleFactor: scaleFactor, - iconPadding: value.iconPadding, - ), - ) - .toList(), - ); - } -} - -class TypeOptionMenuItem extends StatelessWidget { - const TypeOptionMenuItem({ - super.key, - 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; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => value.onTap(context, value.value), - child: Column( - children: [ - Container( - height: scaledIconWidth, - width: scaledIconWidth, - decoration: ShapeDecoration( - color: value.backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24 * scaleFactor), - ), - ), - padding: EdgeInsets.all(21 * scaleFactor) + - (iconPadding ?? EdgeInsets.zero), - child: FlowySvg( - value.icon, - ), - ), - const VSpace(6), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: scaledWidth, - ), - child: FlowyText( - value.text, - fontSize: 14.0, - maxLines: 2, - lineHeight: 1.0, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - ), - ], - ), - ); - } -} - -class TypeOptionGridView extends StatelessWidget { - const TypeOptionGridView({ - super.key, - required this.children, - required this.crossAxisCount, - required this.mainAxisSpacing, - required this.itemWidth, - }); - - final List children; - final int crossAxisCount; - final double mainAxisSpacing; - final double itemWidth; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (var i = 0; i < children.length; i += crossAxisCount) - Padding( - padding: EdgeInsets.only(bottom: mainAxisSpacing), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - for (var j = 0; j < crossAxisCount; j++) - i + j < children.length - ? SizedBox( - width: itemWidth, - child: children[i + j], - ) - : HSpace(itemWidth), - ], - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index a91fbf577b..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.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/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/app_bar/app_bar_actions.dart'; -import 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.dart'; -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'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class MobileViewPageImmersiveAppBar extends StatelessWidget - implements PreferredSizeWidget { - const MobileViewPageImmersiveAppBar({ - super.key, - required this.preferredSize, - 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; - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: appBarOpacity, - builder: (_, opacity, __) => FlowyAppBar( - backgroundColor: - AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity), - showDivider: false, - title: _buildTitle(context, opacity: opacity), - leadingWidth: 44, - leading: Padding( - padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0), - child: _buildAppBarBackButton(context), - ), - actions: actions, - ), - ); - } - - Widget _buildTitle( - BuildContext context, { - required double opacity, - }) { - return title; - } - - Widget _buildAppBarBackButton(BuildContext context) { - return AppBarButton( - padding: EdgeInsets.zero, - onTap: (context) => context.pop(), - child: _ImmersiveAppBarButton( - icon: FlowySvgs.m_app_bar_back_s, - dimension: 30.0, - iconPadding: 3.0, - isImmersiveMode: - context.read().state.isImmersiveMode, - appBarOpacity: appBarOpacity, - ), - ); - } -} - -class MobileViewPageMoreButton extends StatelessWidget { - const MobileViewPageMoreButton({ - super.key, - required this.view, - required this.isImmersiveMode, - required this.appBarOpacity, - }); - - final ViewPB view; - final bool isImmersiveMode; - final ValueListenable appBarOpacity; - - @override - Widget build(BuildContext context) { - return AppBarButton( - padding: const EdgeInsets.only(left: 8, right: 16), - onTap: (context) { - EditorNotification.exitEditing().post(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - backgroundColor: AFThemeExtension.of(context).background, - builder: (_) => MultiBlocProvider( - 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), - ), - ); - }, - child: _ImmersiveAppBarButton( - icon: FlowySvgs.m_app_bar_more_s, - dimension: 30.0, - iconPadding: 3.0, - isImmersiveMode: isImmersiveMode, - appBarOpacity: appBarOpacity, - ), - ); - } -} - -class MobileViewPageLayoutButton extends StatelessWidget { - const MobileViewPageLayoutButton({ - super.key, - required this.view, - required this.isImmersiveMode, - required this.appBarOpacity, - required this.tabs, - }); - - final ViewPB view; - final List tabs; - final bool isImmersiveMode; - final ValueListenable appBarOpacity; - - @override - Widget build(BuildContext context) { - // only display the layout button if the view is a document - if (view.layout != ViewLayoutPB.Document) { - return const SizedBox.shrink(); - } - - return AppBarButton( - padding: const EdgeInsets.symmetric(vertical: 2.0), - onTap: (context) { - EditorNotification.exitEditing().post(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - showDoneButton: true, - showHeader: true, - title: LocaleKeys.pageStyle_title.tr(), - backgroundColor: AFThemeExtension.of(context).background, - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: context.read()), - BlocProvider.value(value: context.read()), - ], - child: PageStyleBottomSheet( - view: context.read().state.view, - tabs: tabs, - ), - ), - ); - }, - child: _ImmersiveAppBarButton( - icon: FlowySvgs.m_layout_s, - dimension: 30.0, - iconPadding: 3.0, - isImmersiveMode: isImmersiveMode, - appBarOpacity: appBarOpacity, - ), - ); - } -} - -class _ImmersiveAppBarButton extends StatelessWidget { - const _ImmersiveAppBarButton({ - required this.icon, - required this.dimension, - required this.iconPadding, - required this.isImmersiveMode, - required this.appBarOpacity, - }); - - final FlowySvgData icon; - final double dimension; - final double iconPadding; - final bool isImmersiveMode; - final ValueListenable appBarOpacity; - - @override - Widget build(BuildContext context) { - assert( - dimension > 0.0 && dimension <= kToolbarHeight, - 'dimension must be greater than 0, and less than or equal to kToolbarHeight', - ); - - // if the immersive mode is on, the icon should be white and add a black background - // also, the icon opacity will change based on the app bar opacity - return UnconstrainedBox( - child: SizedBox.square( - dimension: dimension, - child: ValueListenableBuilder( - valueListenable: appBarOpacity, - builder: (context, appBarOpacity, child) { - Color? color; - - // if there's no cover or the cover is not immersive, - // make sure the app bar is always visible - if (!isImmersiveMode) { - color = null; - } else if (appBarOpacity < 0.99) { - color = Colors.white; - } - - Widget child = Container( - margin: EdgeInsets.all(iconPadding), - child: FlowySvg(icon, color: color), - ); - - if (isImmersiveMode && appBarOpacity <= 0.99) { - child = DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(dimension / 2.0), - color: Colors.black.withValues(alpha: 0.2), - ), - child: child, - ); - } - - return 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 deleted file mode 100644 index be134e0a92..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ /dev/null @@ -1,351 +0,0 @@ -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}); - - final ViewPB view; - - @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); - context.pop(); - }, - ), - ), - ); - } - - 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.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.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart deleted file mode 100644 index 4c61f437a8..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart +++ /dev/null @@ -1,10 +0,0 @@ -export 'bottom_sheet_action_widget.dart'; -export 'bottom_sheet_add_new_page.dart'; -export 'bottom_sheet_drag_handler.dart'; -export 'bottom_sheet_rename_widget.dart'; -export 'bottom_sheet_view_item.dart'; -export 'bottom_sheet_view_item_body.dart'; -export 'bottom_sheet_view_page.dart'; -export 'default_mobile_action_pane.dart'; -export 'show_mobile_bottom_sheet.dart'; -export 'show_transition_bottom_sheet.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart deleted file mode 100644 index 6b54b1fda3..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class BottomSheetActionWidget extends StatelessWidget { - const BottomSheetActionWidget({ - super.key, - this.svg, - required this.text, - required this.onTap, - this.iconColor, - }); - - final FlowySvgData? svg; - final String text; - final VoidCallback onTap; - final Color? iconColor; - - @override - Widget build(BuildContext context) { - final iconColor = - this.iconColor ?? AFThemeExtension.of(context).onBackground; - - if (svg == null) { - return OutlinedButton( - style: Theme.of(context) - .outlinedButtonTheme - .style - ?.copyWith(alignment: Alignment.center), - onPressed: onTap, - child: FlowyText( - text, - textAlign: TextAlign.center, - ), - ); - } - - return OutlinedButton.icon( - icon: FlowySvg( - svg!, - size: const Size.square(22.0), - color: iconColor, - ), - label: FlowyText( - text, - overflow: TextOverflow.ellipsis, - ), - style: Theme.of(context) - .outlinedButtonTheme - .style - ?.copyWith(alignment: Alignment.centerLeft), - onPressed: onTap, - ); - } -} 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 deleted file mode 100644 index 3316b7049b..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart +++ /dev/null @@ -1,80 +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_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class AddNewPageWidgetBottomSheet extends StatelessWidget { - const AddNewPageWidgetBottomSheet({ - super.key, - required this.view, - required this.onAction, - }); - - final ViewPB view; - final void Function(ViewLayoutPB layout) onAction; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - FlowyOptionTile.text( - text: LocaleKeys.document_menuName.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.icon_document_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction(ViewLayoutPB.Document), - ), - FlowyOptionTile.text( - text: LocaleKeys.grid_menuName.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.icon_grid_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction(ViewLayoutPB.Grid), - ), - FlowyOptionTile.text( - text: LocaleKeys.board_menuName.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.icon_board_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction(ViewLayoutPB.Board), - ), - FlowyOptionTile.text( - text: LocaleKeys.calendar_menuName.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.icon_calendar_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction(ViewLayoutPB.Calendar), - ), - FlowyOptionTile.text( - text: LocaleKeys.chat_newChat.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.chat_ai_page_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction(ViewLayoutPB.Chat), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart deleted file mode 100644 index b8d0699969..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart +++ /dev/null @@ -1,78 +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 BlockActionBottomSheetType { - delete, - duplicate, - insertAbove, - insertBelow, -} - -// Only works on mobile. -class BlockActionBottomSheet extends StatelessWidget { - const BlockActionBottomSheet({ - super.key, - required this.onAction, - this.extendActionWidgets = const [], - }); - - final void Function(BlockActionBottomSheetType layout) onAction; - final List extendActionWidgets; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // insert above, insert below - FlowyOptionTile.text( - text: LocaleKeys.button_insertAbove.tr(), - leftIcon: const FlowySvg( - FlowySvgs.arrow_up_s, - size: Size.square(20), - ), - showTopBorder: false, - onTap: () => onAction(BlockActionBottomSheetType.insertAbove), - ), - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.button_insertBelow.tr(), - leftIcon: const FlowySvg( - FlowySvgs.arrow_down_s, - size: Size.square(20), - ), - onTap: () => onAction(BlockActionBottomSheetType.insertBelow), - ), - // duplicate, delete - 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), - ), - ), - onTap: () => onAction(BlockActionBottomSheetType.duplicate), - ), - - ...extendActionWidgets, - - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.button_delete.tr(), - leftIcon: FlowySvg( - FlowySvgs.trash_s, - size: const Size.square(18), - color: Theme.of(context).colorScheme.error, - ), - textColor: Theme.of(context).colorScheme.error, - onTap: () => onAction(BlockActionBottomSheetType.delete), - ), - ], - ); - } -} 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 deleted file mode 100644 index 8adc2bebec..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class BottomSheetCloseButton extends StatelessWidget { - const BottomSheetCloseButton({ - super.key, - this.onTap, - }); - - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap ?? () => Navigator.pop(context), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - width: 18, - height: 18, - child: FlowySvg( - FlowySvgs.m_bottom_sheet_close_m, - ), - ), - ), - ); - } -} - -class BottomSheetDoneButton extends StatelessWidget { - const BottomSheetDoneButton({ - super.key, - this.onDone, - }); - - final VoidCallback? onDone; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onDone ?? () => Navigator.pop(context), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0), - child: FlowyText( - LocaleKeys.button_done.tr(), - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500, - textAlign: TextAlign.right, - ), - ), - ); - } -} - -class BottomSheetRemoveButton extends StatelessWidget { - const BottomSheetRemoveButton({ - super.key, - required this.onRemove, - }); - - final VoidCallback onRemove; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onRemove, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0), - child: FlowyText( - LocaleKeys.button_remove.tr(), - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500, - textAlign: TextAlign.right, - ), - ), - ); - } -} - -class BottomSheetBackButton extends StatelessWidget { - const BottomSheetBackButton({ - super.key, - this.onTap, - }); - - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap ?? () => Navigator.pop(context), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - width: 18, - height: 18, - child: FlowySvg( - FlowySvgs.m_bottom_sheet_back_s, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart deleted file mode 100644 index 4e9fcd3d7e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -class MobileBottomSheetDragHandler extends StatelessWidget { - const MobileBottomSheetDragHandler({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Container( - width: 60, - height: 4, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.0), - color: Theme.of(context).hintColor, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart deleted file mode 100644 index bed69e1cc4..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart +++ /dev/null @@ -1,104 +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_header.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'; - -class MobileBottomSheetEditLinkWidget extends StatefulWidget { - const MobileBottomSheetEditLinkWidget({ - super.key, - required this.text, - required this.href, - required this.onEdit, - }); - - final String text; - final String? href; - final void Function(String text, String href) onEdit; - - @override - State createState() => - _MobileBottomSheetEditLinkWidgetState(); -} - -class _MobileBottomSheetEditLinkWidgetState - extends State { - late final TextEditingController textController; - late final TextEditingController hrefController; - - @override - void initState() { - super.initState(); - - textController = TextEditingController( - text: widget.text, - ); - hrefController = TextEditingController( - text: widget.href, - ); - } - - @override - void dispose() { - textController.dispose(); - hrefController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - BottomSheetHeader( - title: LocaleKeys.editor_editLink.tr(), - onClose: () => context.pop(), - onDone: () { - widget.onEdit(textController.text, hrefController.text); - }, - ), - const VSpace(20.0), - _buildTextField( - textController, - LocaleKeys.document_inlineLink_title_placeholder.tr(), - ), - const VSpace(12.0), - _buildTextField( - hrefController, - LocaleKeys.document_inlineLink_url_placeholder.tr(), - ), - const VSpace(12.0), - ], - ); - } - - Widget _buildTextField( - TextEditingController controller, - String? hintText, - ) { - return SizedBox( - height: 48.0, - child: FlowyTextField( - controller: controller, - hintText: hintText, - textStyle: const TextStyle(fontSize: 16.0), - hintStyle: const TextStyle(fontSize: 16.0), - suffixIcon: Padding( - padding: const EdgeInsets.all(4.0), - child: FlowyButton( - text: const FlowySvg( - FlowySvgs.close_lg, - ), - margin: EdgeInsets.zero, - useIntrinsicWidth: true, - onTap: () { - controller.clear(); - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart deleted file mode 100644 index e1bc32a6f0..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class BottomSheetHeader extends StatelessWidget { - const BottomSheetHeader({ - super.key, - this.title, - this.onClose, - this.onDone, - }); - - final String? title; - final VoidCallback? onClose; - final VoidCallback? onDone; - - @override - Widget build(BuildContext context) { - return Stack( - alignment: Alignment.center, - children: [ - if (onClose != null) - Positioned( - left: 0, - child: Align( - alignment: Alignment.centerLeft, - child: BottomSheetCloseButton( - onTap: onClose, - ), - ), - ), - if (title != null) - Align( - child: FlowyText.medium( - title!, - fontSize: 16, - ), - ), - if (onDone != null) - Align( - alignment: Alignment.centerRight, - child: BottomSheetDoneButton( - onDone: onDone, - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index 8ac4d9b20e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart +++ /dev/null @@ -1,78 +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'; - -class MobileBottomSheetRenameWidget extends StatefulWidget { - const MobileBottomSheetRenameWidget({ - super.key, - required this.name, - required this.onRename, - this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0), - }); - - final String name; - final void Function(String name) onRename; - final EdgeInsets padding; - - @override - State createState() => - _MobileBottomSheetRenameWidgetState(); -} - -class _MobileBottomSheetRenameWidgetState - extends State { - late final TextEditingController controller; - - @override - void initState() { - super.initState(); - controller = TextEditingController(text: widget.name) - ..selection = TextSelection( - baseOffset: 0, - extentOffset: widget.name.length, - ); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: widget.padding, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: SizedBox( - height: 42.0, - child: FlowyTextField( - controller: controller, - textStyle: Theme.of(context).textTheme.bodyMedium, - keyboardType: TextInputType.text, - onSubmitted: (text) => widget.onRename(text), - ), - ), - ), - const HSpace(12.0), - FlowyTextButton( - LocaleKeys.button_edit.tr(), - constraints: const BoxConstraints.tightFor(height: 42), - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - fontColor: Colors.white, - fillColor: Theme.of(context).primaryColor, - onPressed: () { - widget.onRename(controller.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 deleted file mode 100644 index 86021ea938..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart +++ /dev/null @@ -1,152 +0,0 @@ -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/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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:fluttertoast/fluttertoast.dart'; - -enum MobileBottomSheetType { - view, - rename, -} - -class MobileViewItemBottomSheet extends StatefulWidget { - const MobileViewItemBottomSheet({ - super.key, - required this.view, - required this.actions, - this.defaultType = MobileBottomSheetType.view, - }); - - final ViewPB view; - final MobileBottomSheetType defaultType; - final List actions; - - @override - State createState() => - _MobileViewItemBottomSheetState(); -} - -class _MobileViewItemBottomSheetState extends State { - MobileBottomSheetType type = MobileBottomSheetType.view; - final fToast = FToast(); - - @override - void initState() { - super.initState(); - - type = widget.defaultType; - fToast.init(AppGlobals.context); - } - - @override - Widget build(BuildContext context) { - switch (type) { - case MobileBottomSheetType.view: - return MobileViewItemBottomSheetBody( - actions: widget.actions, - isFavorite: widget.view.isFavorite, - onAction: (action) { - switch (action) { - case MobileViewItemBottomSheetBodyAction.rename: - setState(() { - type = MobileBottomSheetType.rename; - }); - break; - case MobileViewItemBottomSheetBodyAction.duplicate: - Navigator.pop(context); - context.read().add(const ViewEvent.duplicate()); - showToastNotification( - message: LocaleKeys.button_duplicateSuccessfully.tr(), - ); - break; - case MobileViewItemBottomSheetBodyAction.share: - // unimplemented - Navigator.pop(context); - break; - case MobileViewItemBottomSheetBodyAction.delete: - Navigator.pop(context); - context.read().add(const ViewEvent.delete()); - break; - case MobileViewItemBottomSheetBodyAction.addToFavorites: - case MobileViewItemBottomSheetBodyAction.removeFromFavorites: - Navigator.pop(context); - 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); - break; - case MobileViewItemBottomSheetBodyAction.divider: - break; - } - }, - ); - case MobileBottomSheetType.rename: - return MobileBottomSheetRenameWidget( - name: widget.view.name, - onRename: (name) { - if (name != widget.view.name) { - context.read().add(ViewEvent.rename(name)); - } - Navigator.pop(context); - }, - ); - } - } - - Future _removeFromRecent(BuildContext context) async { - final viewId = context.read().view.id; - final recentViewsBloc = context.read(); - Navigator.pop(context); - - await _showConfirmDialog( - onDelete: () { - recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId])); - }, - ); - } - - Future _showConfirmDialog({required VoidCallback onDelete}) async { - await showFlowyCupertinoConfirmDialog( - title: LocaleKeys.sideBar_removePageFromRecent.tr(), - leftButton: FlowyText( - LocaleKeys.button_cancel.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w500, - color: const Color(0xFF007AFF), - ), - rightButton: FlowyText( - LocaleKeys.button_delete.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w400, - color: const Color(0xFFFE0220), - ), - onRightButtonPressed: (context) { - onDelete(); - - Navigator.pop(context); - - showToastNotification( - message: LocaleKeys.sideBar_removeSuccess.tr(), - ); - }, - ); - } -} 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 deleted file mode 100644 index 0ca60fe40b..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.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/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, - duplicate, - share, - delete, - addToFavorites, - removeFromFavorites, - divider, - removeFromRecent, -} - -class MobileViewItemBottomSheetBody extends StatelessWidget { - const MobileViewItemBottomSheetBody({ - super.key, - this.isFavorite = false, - required this.onAction, - required this.actions, - }); - - final bool isFavorite; - final void Function(MobileViewItemBottomSheetBodyAction 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, - MobileViewItemBottomSheetBodyAction action, - ) { - final isLocked = - context.read()?.state.isLocked ?? false; - switch (action) { - case MobileViewItemBottomSheetBodyAction.rename: - return FlowyOptionTile.text( - text: LocaleKeys.button_rename.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.view_item_rename_s, - size: Size.square(18), - ), - enable: !isLocked, - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.rename, - ), - ); - case MobileViewItemBottomSheetBodyAction.duplicate: - return FlowyOptionTile.text( - text: LocaleKeys.button_duplicate.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.duplicate_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.duplicate, - ), - ); - - case MobileViewItemBottomSheetBodyAction.share: - return FlowyOptionTile.text( - text: LocaleKeys.button_share.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.share_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.share, - ), - ); - case MobileViewItemBottomSheetBodyAction.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, - ), - enable: !isLocked, - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.delete, - ), - ); - case MobileViewItemBottomSheetBodyAction.addToFavorites: - return FlowyOptionTile.text( - height: 52.0, - text: LocaleKeys.button_addToFavorites.tr(), - leftIcon: const FlowySvg( - FlowySvgs.favorite_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.addToFavorites, - ), - ); - case MobileViewItemBottomSheetBodyAction.removeFromFavorites: - return FlowyOptionTile.text( - height: 52.0, - text: LocaleKeys.button_removeFromFavorites.tr(), - leftIcon: const FlowySvg( - FlowySvgs.favorite_section_remove_from_favorite_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.removeFromFavorites, - ), - ); - case MobileViewItemBottomSheetBodyAction.removeFromRecent: - return FlowyOptionTile.text( - height: 52.0, - text: LocaleKeys.button_removeFromRecent.tr(), - leftIcon: const FlowySvg( - FlowySvgs.remove_from_recent_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - MobileViewItemBottomSheetBodyAction.removeFromRecent, - ), - ); - - case MobileViewItemBottomSheetBodyAction.divider: - return const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: Divider(height: 0.5), - ); - } - } -} 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 deleted file mode 100644 index 47ab37505e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ /dev/null @@ -1,289 +0,0 @@ -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, - 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'; -} - -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({ - super.key, - required this.view, - required this.onAction, - required this.onRename, - }); - - final ViewPB view; - final MobileViewBottomSheetBodyActionCallback onAction; - final void Function(String name) onRename; - - @override - State createState() => _ViewPageBottomSheetState(); -} - -class _ViewPageBottomSheetState extends State { - MobileBottomSheetType type = MobileBottomSheetType.view; - - @override - Widget build(BuildContext context) { - switch (type) { - case MobileBottomSheetType.view: - return MobileViewBottomSheetBody( - view: widget.view, - onAction: (action, {arguments}) { - switch (action) { - case MobileViewBottomSheetBodyAction.rename: - setState(() { - type = MobileBottomSheetType.rename; - }); - break; - default: - widget.onAction(action, arguments: arguments); - } - }, - ); - - case MobileBottomSheetType.rename: - return MobileBottomSheetRenameWidget( - name: widget.view.name, - onRename: (name) { - widget.onRename(name); - }, - ); - } - } -} - -class MobileViewBottomSheetBody extends StatelessWidget { - const MobileViewBottomSheetBody({ - super.key, - required this.view, - required this.onAction, - }); - - final ViewPB view; - final MobileViewBottomSheetBodyActionCallback onAction; - - @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, - ), - ), - _divider(), - MobileQuickActionButton( - text: isFavorite - ? 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 - : MobileViewBottomSheetBodyAction.addToFavorites, - ), - ), - _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, - ), - ), - _divider(), - ], - ); - } - - 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, - }, - ); - }, - ), - ), - ); - } -} 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 deleted file mode 100644 index d4b4292443..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ /dev/null @@ -1,218 +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/home/shared/mobile_page_card.dart'; -import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.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/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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; - -enum MobilePaneActionType { - delete, - addToFavorites, - removeFromFavorites, - more, - add; - - MobileSlideActionButton actionButton( - BuildContext context, { - MobilePageCardType? cardType, - FolderSpaceType? spaceType, - }) { - switch (this) { - case MobilePaneActionType.delete: - return MobileSlideActionButton( - backgroundColor: Colors.red, - svg: FlowySvgs.delete_s, - size: 30.0, - onPressed: (context) => - context.read().add(const ViewEvent.delete()), - ); - case MobilePaneActionType.removeFromFavorites: - return MobileSlideActionButton( - 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)); - }, - ); - 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)); - }, - ); - case MobilePaneActionType.add: - return MobileSlideActionButton( - backgroundColor: const Color(0xFF00C8FF), - svg: FlowySvgs.add_m, - size: 28.0, - onPressed: (context) { - final viewBloc = context.read(); - final view = viewBloc.state.view; - final title = view.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: view, - onAction: (layout) { - Navigator.of(sheetContext).pop(); - viewBloc.add( - ViewEvent.createView( - layout.defaultName, - layout, - section: spaceType!.toViewSectionPB, - ), - ); - }, - ); - }, - ); - }, - ); - case MobilePaneActionType.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 viewBloc = context.read(); - final favoriteBloc = context.read(); - final recentViewsBloc = context.read(); - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - useRootNavigator: true, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: viewBloc), - 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) { - return MobileViewItemBottomSheet( - view: viewBloc.state.view, - actions: _buildActions(state.view, cardType: cardType), - ); - }, - ), - ); - }, - ); - }, - ); - } - } - - List _buildActions( - ViewPB view, { - MobilePageCardType? cardType, - }) { - final isFavorite = view.isFavorite; - - if (cardType != null) { - switch (cardType) { - case MobilePageCardType.recent: - return [ - isFavorite - ? MobileViewItemBottomSheetBodyAction.removeFromFavorites - : MobileViewItemBottomSheetBodyAction.addToFavorites, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.removeFromRecent, - ]; - case MobilePageCardType.favorite: - return [ - isFavorite - ? MobileViewItemBottomSheetBodyAction.removeFromFavorites - : MobileViewItemBottomSheetBodyAction.addToFavorites, - MobileViewItemBottomSheetBodyAction.divider, - ]; - } - } - - return [ - isFavorite - ? MobileViewItemBottomSheetBodyAction.removeFromFavorites - : MobileViewItemBottomSheetBodyAction.addToFavorites, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.rename, - if (view.layout != ViewLayoutPB.Chat) - MobileViewItemBottomSheetBodyAction.duplicate, - MobileViewItemBottomSheetBodyAction.divider, - MobileViewItemBottomSheetBodyAction.delete, - ]; - } -} - -ActionPane buildEndActionPane( - BuildContext context, - List actions, { - bool needSpace = true, - MobilePageCardType? cardType, - FolderSpaceType? spaceType, - required double spaceRatio, -}) { - return ActionPane( - motion: const ScrollMotion(), - extentRatio: actions.length / spaceRatio, - children: [ - if (needSpace) const HSpace(60), - ...actions.map( - (action) => action.actionButton( - context, - spaceType: spaceType, - cardType: cardType, - ), - ), - ], - ); -} 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 deleted file mode 100644 index a0fa5dc6aa..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ /dev/null @@ -1,294 +0,0 @@ -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; -import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -extension BottomSheetPaddingExtension on BuildContext { - /// Calculates the total amount of space that should be added to the bottom of - /// a bottom sheet - double bottomSheetPadding({ - bool ignoreViewPadding = true, - }) { - final viewPadding = MediaQuery.viewPaddingOf(this); - final viewInsets = MediaQuery.viewInsetsOf(this); - double bottom = 0.0; - if (!ignoreViewPadding) { - bottom += viewPadding.bottom; - } - // for screens with 0 view padding, add some even more space - bottom += viewPadding.bottom == 0 ? 28.0 : 16.0; - bottom += viewInsets.bottom; - return bottom; - } -} - -Future showMobileBottomSheet( - BuildContext context, { - required WidgetBuilder builder, - bool useSafeArea = true, - bool isDragEnabled = true, - bool showDragHandle = false, - bool showHeader = false, - // this field is only used if showHeader is true - bool showBackButton = false, - bool showCloseButton = false, - bool showRemoveButton = false, - VoidCallback? onRemove, - // this field is only used if showHeader is true - String title = '', - bool isScrollControlled = true, - bool showDivider = true, - bool useRootNavigator = false, - ShapeBorder? shape, - // the padding of the content, the padding of the header area is fixed - EdgeInsets padding = EdgeInsets.zero, - Color? backgroundColor, - BoxConstraints? constraints, - 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 || - title.isEmpty && !showCloseButton && !showBackButton && !showDoneButton, - ); - assert(!(showCloseButton && showBackButton)); - - shape ??= const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(16), - ), - ); - - backgroundColor ??= Theme.of(context).brightness == Brightness.light - ? const Color(0xFFF7F8FB) - : const Color(0xFF23262B); - barrierColor ??= Colors.black.withValues(alpha: 0.3); - - return showModalBottomSheet( - context: context, - isScrollControlled: isScrollControlled, - enableDrag: isDragEnabled, - useSafeArea: true, - clipBehavior: Clip.antiAlias, - constraints: constraints, - barrierColor: barrierColor, - elevation: elevation, - backgroundColor: backgroundColor, - shape: shape, - useRootNavigator: useRootNavigator, - builder: (context) { - final List children = []; - - final Widget child = builder(context); - - // if the children is only one, we don't need to wrap it with a column - if (!showDragHandle && !showHeader && !showDivider) { - return child; - } - - // ----- header area ----- - if (showDragHandle) { - children.add( - const DragHandle(), - ); - } - - if (showHeader) { - children.add( - BottomSheetHeader( - showCloseButton: showCloseButton, - showBackButton: showBackButton, - showDoneButton: showDoneButton, - showRemoveButton: showRemoveButton, - title: title, - onRemove: onRemove, - onDone: onDone, - ), - ); - - if (showDivider) { - children.add( - const Divider(height: 0.5, thickness: 0.5), - ); - } - } - - // ----- header area ----- - - if (enableDraggableScrollable) { - final keyboardSize = - context.bottomSheetPadding() / MediaQuery.of(context).size.height; - return DraggableScrollableSheet( - expand: false, - snap: true, - initialChildSize: (initialChildSize + keyboardSize).clamp(0, 1), - minChildSize: (minChildSize + keyboardSize).clamp(0, 1.0), - maxChildSize: (maxChildSize + keyboardSize).clamp(0, 1.0), - builder: (context, scrollController) { - return Column( - children: [ - ...children, - scrollableWidgetBuilder?.call( - context, - scrollController, - ) ?? - Expanded( - child: Scrollbar( - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: child, - ), - ), - ), - ], - ); - }, - ); - } 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); - } - // ----- content area ----- - - if (children.length == 1) { - return children.first; - } - - return useSafeArea - ? SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: children, - ); - }, - ); -} - -class BottomSheetHeader extends StatelessWidget { - const BottomSheetHeader({ - super.key, - required this.showBackButton, - required this.showCloseButton, - required this.showRemoveButton, - 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 bool showDoneButton; - - final VoidCallback? onRemove; - final VoidCallback? onBack; - final VoidCallback? onClose; - - final void Function(BuildContext context)? onDone; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: SizedBox( - height: 44.0, // the height of the header area is fixed - child: Stack( - children: [ - if (showBackButton) - Align( - alignment: Alignment.centerLeft, - child: BottomSheetBackButton( - onTap: onBack, - ), - ), - if (showCloseButton) - Align( - alignment: Alignment.centerLeft, - child: BottomSheetCloseButton( - onTap: onClose, - ), - ), - if (showRemoveButton) - Align( - alignment: Alignment.centerLeft, - child: BottomSheetRemoveButton( - onRemove: () => onRemove?.call(), - ), - ), - Align( - child: Container( - constraints: const BoxConstraints(maxWidth: 250), - child: FlowyText( - title, - fontSize: 17.0, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), - ), - ), - if (showDoneButton) - Align( - alignment: Alignment.centerRight, - child: BottomSheetDoneButton( - onDone: () { - if (onDone != null) { - onDone?.call(context); - } else { - 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 deleted file mode 100644 index b29817251a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart +++ /dev/null @@ -1,346 +0,0 @@ -import 'dart:math' as math; - -import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:sheet/route.dart'; -import 'package:sheet/sheet.dart'; - -import 'show_mobile_bottom_sheet.dart'; - -Future showTransitionMobileBottomSheet( - BuildContext context, { - required WidgetBuilder builder, - bool useRootNavigator = false, - EdgeInsets contentPadding = EdgeInsets.zero, - Color? backgroundColor, - // drag handle - bool showDragHandle = false, - // header - bool showHeader = false, - String title = '', - bool showBackButton = false, - bool showCloseButton = false, - bool showDoneButton = false, - bool showDivider = true, - // stops - double initialStop = 1.0, - List? stops, -}) { - assert( - showHeader || - title.isEmpty && - !showCloseButton && - !showBackButton && - !showDoneButton && - !showDivider, - ); - assert(!(showCloseButton && showBackButton)); - - backgroundColor ??= Theme.of(context).brightness == Brightness.light - ? const Color(0xFFF7F8FB) - : const Color(0xFF23262B); - - return Navigator.of( - context, - rootNavigator: useRootNavigator, - ).push( - TransitionSheetRoute( - backgroundColor: backgroundColor, - initialStop: initialStop, - stops: stops, - builder: (context) { - final Widget child = builder(context); - - // if the children is only one, we don't need to wrap it with a column - if (!showDragHandle && !showHeader) { - return child; - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (showDragHandle) const DragHandle(), - if (showHeader) ...[ - BottomSheetHeader( - showCloseButton: showCloseButton, - showBackButton: showBackButton, - showDoneButton: showDoneButton, - showRemoveButton: false, - title: title, - ), - if (showDivider) - const Divider( - height: 0.5, - thickness: 0.5, - ), - ], - Expanded( - child: Padding( - padding: contentPadding, - child: child, - ), - ), - ], - ); - }, - ), - ); -} - -/// The top offset that will be displayed from the bottom route -const double _kPreviousRouteVisibleOffset = 10.0; - -/// Minimal distance from the top of the screen to the top of the previous route -/// It will be used ff the top safe area is less than this value. -/// In iPhones the top SafeArea is more or equal to this distance. -const double _kSheetMinimalOffset = 10; - -const Curve _kCupertinoSheetCurve = Curves.easeOutExpo; -const Curve _kCupertinoTransitionCurve = Curves.linear; - -/// Wraps the child into a cupertino modal sheet appearance. This is used to -/// create a [SheetRoute]. -/// -/// Clip the child widget to rectangle with top rounded corners and adds -/// top padding and top safe area. -class _CupertinoSheetDecorationBuilder extends StatelessWidget { - const _CupertinoSheetDecorationBuilder({ - required this.child, - required this.topRadius, - this.backgroundColor, - }); - - /// The child contained by the modal sheet - final Widget child; - - /// The color to paint behind the child - final Color? backgroundColor; - - /// The top corners of this modal sheet are rounded by this Radius - final Radius topRadius; - - @override - Widget build(BuildContext context) { - return Builder( - builder: (BuildContext context) { - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.vertical(top: topRadius), - color: backgroundColor, - ), - child: MediaQuery.removePadding( - context: context, - removeTop: true, - child: child, - ), - ); - }, - ); - } -} - -/// Customized CupertinoSheetRoute from the sheets package -/// -/// A modal route that overlays a widget over the current route and animates -/// it from the bottom with a cupertino modal sheet appearance -/// -/// Clip the child widget to rectangle with top rounded corners and adds -/// top padding and top safe area. -class TransitionSheetRoute extends SheetRoute { - TransitionSheetRoute({ - required WidgetBuilder builder, - super.stops, - double initialStop = 1.0, - super.settings, - Color? backgroundColor, - super.maintainState = true, - super.fit, - }) : super( - builder: (BuildContext context) { - return _CupertinoSheetDecorationBuilder( - backgroundColor: backgroundColor, - topRadius: const Radius.circular(16), - child: Builder(builder: builder), - ); - }, - animationCurve: _kCupertinoSheetCurve, - initialExtent: initialStop, - ); - - @override - bool get draggable => true; - - final SheetController _sheetController = SheetController(); - - @override - SheetController createSheetController() => _sheetController; - - @override - Color? get barrierColor => Colors.transparent; - - @override - bool get barrierDismissible => true; - - @override - Widget buildSheet(BuildContext context, Widget child) { - final effectivePhysics = draggable - ? BouncingSheetPhysics( - parent: SnapSheetPhysics( - stops: stops ?? [0, 1], - parent: physics, - ), - ) - : const NeverDraggableSheetPhysics(); - final MediaQueryData mediaQuery = MediaQuery.of(context); - final double topMargin = - math.max(_kSheetMinimalOffset, mediaQuery.padding.top) + - _kPreviousRouteVisibleOffset; - return Sheet.raw( - initialExtent: initialExtent, - decorationBuilder: decorationBuilder, - fit: fit, - maxExtent: mediaQuery.size.height - topMargin, - physics: effectivePhysics, - controller: sheetController, - child: child, - ); - } - - @override - Widget buildTransitions( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - final double topPadding = MediaQuery.of(context).padding.top; - final double topOffset = math.max(_kSheetMinimalOffset, topPadding); - return AnimatedBuilder( - animation: secondaryAnimation, - child: child, - builder: (BuildContext context, Widget? child) { - final double progress = secondaryAnimation.value; - final double scale = 1 - progress / 10; - final double distanceWithScale = - (topOffset + _kPreviousRouteVisibleOffset) * 0.9; - final Offset offset = - Offset(0, progress * (topOffset - distanceWithScale)); - return Transform.translate( - offset: offset, - child: Transform.scale( - scale: scale, - alignment: Alignment.topCenter, - child: child, - ), - ); - }, - ); - } - - @override - bool canDriveSecondaryTransitionForPreviousRoute( - Route previousRoute, - ) => - true; - - @override - Widget buildSecondaryTransitionForPreviousRoute( - BuildContext context, - Animation secondaryAnimation, - Widget child, - ) { - final Animation delayAnimation = CurvedAnimation( - parent: _sheetController.animation, - curve: Interval( - initialExtent == 1 ? 0 : initialExtent, - 1, - ), - ); - - final Animation secondaryAnimation = CurvedAnimation( - parent: _sheetController.animation, - curve: Interval( - 0, - initialExtent, - ), - ); - - return CupertinoSheetBottomRouteTransition( - body: child, - sheetAnimation: delayAnimation, - secondaryAnimation: secondaryAnimation, - ); - } -} - -/// Animation for previous route when a [TransitionSheetRoute] enters/exits -@visibleForTesting -class CupertinoSheetBottomRouteTransition extends StatelessWidget { - const CupertinoSheetBottomRouteTransition({ - super.key, - required this.sheetAnimation, - required this.secondaryAnimation, - required this.body, - }); - - final Widget body; - - final Animation sheetAnimation; - final Animation secondaryAnimation; - - @override - Widget build(BuildContext context) { - final double topPadding = MediaQuery.of(context).padding.top; - final double topOffset = math.max(_kSheetMinimalOffset, topPadding); - - final CurvedAnimation curvedAnimation = CurvedAnimation( - parent: sheetAnimation, - curve: _kCupertinoTransitionCurve, - ); - - return AnnotatedRegion( - value: SystemUiOverlayStyle.light, - child: AnimatedBuilder( - animation: secondaryAnimation, - child: body, - builder: (BuildContext context, Widget? child) { - final double progress = curvedAnimation.value; - final double scale = 1 - progress / 10; - return Stack( - children: [ - Container(color: Colors.black), - Transform.translate( - offset: Offset(0, progress * topOffset), - child: Transform.scale( - scale: scale, - alignment: Alignment.topCenter, - child: ClipRRect( - borderRadius: BorderRadius.vertical( - top: Radius.lerp( - Radius.zero, - const Radius.circular(16.0), - progress, - )!, - ), - child: ColorFiltered( - colorFilter: ColorFilter.mode( - (Theme.of(context).brightness == Brightness.dark - ? Colors.grey - : Colors.black) - .withValues(alpha: secondaryAnimation.value * 0.1), - BlendMode.srcOver, - ), - child: child, - ), - ), - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/chat/mobile_chat_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/chat/mobile_chat_screen.dart deleted file mode 100644 index 31fcbdcdfd..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/chat/mobile_chat_screen.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - -class MobileChatScreen extends StatelessWidget { - const MobileChatScreen({ - super.key, - required this.id, - this.title, - }); - - /// view id - final String id; - final String? title; - - static const routeName = '/chat'; - static const viewId = 'id'; - static const viewTitle = 'title'; - - @override - Widget build(BuildContext context) { - return MobileViewPage( - id: id, - title: title, - viewLayout: ViewLayoutPB.Chat, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart deleted file mode 100644 index 642a7ebeae..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'mobile_board_screen.dart'; -export 'mobile_board_page.dart'; -export 'widgets/mobile_hidden_groups_column.dart'; -export 'widgets/mobile_board_trailing.dart'; 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 deleted file mode 100644 index 29841dd22a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart +++ /dev/null @@ -1,317 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -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/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.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'; -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'; - -class MobileBoardPage extends StatefulWidget { - const MobileBoardPage({ - super.key, - required this.view, - required this.databaseController, - this.onEditStateChanged, - }); - - final ViewPB view; - - final DatabaseController databaseController; - - /// Called when edit state changed - final VoidCallback? onEditStateChanged; - - @override - State createState() => _MobileBoardPageState(); -} - -class _MobileBoardPageState extends State { - late final ValueNotifier _didCreateRow; - - @override - void initState() { - super.initState(); - _didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow); - } - - @override - void dispose() { - _didCreateRow - ..removeListener(_handleDidCreateRow) - ..dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => BoardBloc( - databaseController: widget.databaseController, - didCreateRow: _didCreateRow, - )..add(const BoardEvent.initial()), - child: BlocBuilder( - builder: (context, state) => state.maybeMap( - loading: (_) => const Center( - child: CircularProgressIndicator.adaptive(), - ), - error: (err) => Center( - child: AppFlowyErrorPage( - error: err.error, - ), - ), - ready: (data) => const _BoardContent(), - orElse: () => const SizedBox.shrink(), - ), - ), - ); - } - - void _handleDidCreateRow() { - if (_didCreateRow.value != null) { - final result = _didCreateRow.value!; - switch (result.action) { - case DidCreateRowAction.openAsPage: - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: result.rowMeta.id, - MobileRowDetailPage.argDatabaseController: - widget.databaseController, - }, - ); - break; - default: - break; - } - } - } -} - -class _BoardContent extends StatefulWidget { - const _BoardContent(); - - @override - State<_BoardContent> createState() => _BoardContentState(); -} - -class _BoardContentState extends State<_BoardContent> { - late final ScrollController scrollController; - - @override - void initState() { - super.initState(); - scrollController = ScrollController(); - } - - @override - void dispose() { - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - final config = AppFlowyBoardConfig( - groupCornerRadius: 8, - groupBackgroundColor: Theme.of(context).colorScheme.secondary, - groupMargin: const EdgeInsets.fromLTRB(4, 0, 4, 12), - groupHeaderPadding: const EdgeInsets.all(8), - groupBodyPadding: const EdgeInsets.all(4), - groupFooterPadding: const EdgeInsets.all(8), - cardMargin: const EdgeInsets.all(4), - ); - - return BlocBuilder( - builder: (context, state) { - return state.maybeMap( - orElse: () => const SizedBox.shrink(), - ready: (state) { - final isLocked = - context.watch()?.state.isLocked ?? false; - final showCreateGroupButton = context - .read() - .groupingFieldType - ?.canCreateNewGroup ?? - false; - final showHiddenGroups = state.hiddenGroups.isNotEmpty; - return AppFlowyBoard( - scrollController: scrollController, - controller: context.read().boardController, - groupConstraints: - BoxConstraints.tightFor(width: screenWidth * 0.7), - config: config, - leading: showHiddenGroups - ? MobileHiddenGroupsColumn( - padding: config.groupHeaderPadding, - ) - : const HSpace(16), - trailing: showCreateGroupButton && !isLocked - ? const MobileBoardTrailing() - : const HSpace(16), - headerBuilder: (_, groupData) { - final isLocked = - context.read()?.state.isLocked ?? - false; - return IgnorePointer( - ignoring: isLocked, - child: GroupCardHeader( - groupData: groupData, - ), - ); - }, - footerBuilder: _buildFooter, - cardBuilder: (_, column, columnItem) => _buildCard( - context: context, - afGroupData: column, - afGroupItem: columnItem, - cardMargin: config.cardMargin, - ), - ); - }, - ); - }, - ); - } - - 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, - 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, - ), - ), - ), - ), - ); - } - - Widget _buildCard({ - required BuildContext context, - required AppFlowyGroupData afGroupData, - required AppFlowyGroupItem afGroupItem, - required EdgeInsets cardMargin, - }) { - final boardBloc = context.read(); - final groupItem = afGroupItem as GroupItem; - final groupData = afGroupData.customData as GroupData; - final rowMeta = groupItem.row; - - final cellBuilder = - 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, - }, - ); - }, - onStartEditing: () {}, - onEndEditing: () {}, - styleConfiguration: RowCardStyleConfiguration( - cellStyleMap: mobileBoardCardCellStyleMap(context), - showAccessory: false, - ), - userProfile: boardBloc.userProfile, - ), - ), - ), - ); - } - - BoxDecoration _makeBoxDecoration(BuildContext context) { - final themeMode = context.read().state.themeMode; - return BoxDecoration( - color: AFThemeExtension.of(context).background, - borderRadius: const BorderRadius.all(Radius.circular(8)), - border: themeMode == ThemeMode.light - ? Border.fromBorderSide( - BorderSide( - color: Theme.of(context) - .colorScheme - .outline - .withValues(alpha: 0.5), - ), - ) - : null, - boxShadow: themeMode == ThemeMode.light - ? [ - BoxShadow( - color: Theme.of(context) - .colorScheme - .outline - .withValues(alpha: 0.5), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ] - : null, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_screen.dart deleted file mode 100644 index 492a8cb347..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_screen.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - -class MobileBoardScreen extends StatelessWidget { - const MobileBoardScreen({ - super.key, - required this.id, - this.title, - }); - - /// view id - final String id; - final String? title; - - static const routeName = '/board'; - static const viewId = 'id'; - static const viewTitle = 'title'; - - @override - Widget build(BuildContext context) { - return MobileViewPage( - id: id, - title: title, - viewLayout: ViewLayoutPB.Document, - ); - } -} 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 deleted file mode 100644 index 8d1e91b708..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart +++ /dev/null @@ -1,184 +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/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_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_board/appflowy_board.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -// similar to [BoardColumnHeader] in Desktop -class GroupCardHeader extends StatefulWidget { - const GroupCardHeader({ - super.key, - required this.groupData, - }); - - final AppFlowyGroupData groupData; - - @override - State createState() => _GroupCardHeaderState(); -} - -class _GroupCardHeaderState extends State { - late final TextEditingController _controller = - TextEditingController.fromValue( - TextEditingValue( - selection: TextSelection.collapsed( - offset: widget.groupData.headerData.groupName.length, - ), - text: widget.groupData.headerData.groupName, - ), - ); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final boardCustomData = widget.groupData.customData as GroupData; - final titleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.w600, - ); - return BlocBuilder( - builder: (context, state) { - Widget title = Text( - widget.groupData.headerData.groupName, - style: titleTextStyle, - overflow: TextOverflow.ellipsis, - ); - - // header can be edited if it's not default group(no status) and the field type can be edited - if (!boardCustomData.group.isDefault && - boardCustomData.fieldType.canEditHeader) { - title = GestureDetector( - onTap: () => context - .read() - .add(BoardEvent.startEditingHeader(widget.groupData.id)), - child: Text( - widget.groupData.headerData.groupName, - style: titleTextStyle, - overflow: TextOverflow.ellipsis, - ), - ); - } - - final isEditing = state.maybeMap( - ready: (value) => value.editingHeaderId == widget.groupData.id, - orElse: () => false, - ); - - if (isEditing) { - title = TextField( - controller: _controller, - autofocus: true, - onEditingComplete: () => context.read().add( - BoardEvent.endEditingHeader( - widget.groupData.id, - _controller.text, - ), - ), - style: titleTextStyle, - onTapOutside: (_) => context.read().add( - // group header switch from TextField to Text - // group name won't be changed - BoardEvent.endEditingHeader(widget.groupData.id, null), - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(left: 16), - child: SizedBox( - height: 42, - child: Row( - children: [ - _buildHeaderIcon(boardCustomData), - Expanded(child: title), - IconButton( - icon: Icon( - Icons.more_horiz_rounded, - color: Theme.of(context).colorScheme.onSurface, - ), - splashRadius: 5, - onPressed: () => showMobileBottomSheet( - context, - showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (_) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - MobileQuickActionButton( - text: LocaleKeys.board_column_renameColumn.tr(), - icon: FlowySvgs.edit_s, - onTap: () { - context.read().add( - BoardEvent.startEditingHeader( - widget.groupData.id, - ), - ); - context.pop(); - }, - ), - const MobileQuickActionDivider(), - MobileQuickActionButton( - text: LocaleKeys.board_column_hideColumn.tr(), - icon: FlowySvgs.hide_s, - onTap: () { - context.read().add( - BoardEvent.setGroupVisibility( - widget.groupData.customData.group - as GroupPB, - false, - ), - ); - context.pop(); - }, - ), - ], - ), - ), - ), - IconButton( - icon: Icon( - Icons.add, - color: Theme.of(context).colorScheme.onSurface, - ), - splashRadius: 5, - onPressed: () { - context.read().add( - BoardEvent.createRow( - widget.groupData.id, - OrderObjectPositionTypePB.Start, - null, - null, - ), - ); - }, - ), - ], - ), - ), - ); - }, - ); - } - - 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(), - }; -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart deleted file mode 100644 index 184bd901c1..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -/// Add new group -class MobileBoardTrailing extends StatefulWidget { - const MobileBoardTrailing({super.key}); - - @override - State createState() => _MobileBoardTrailingState(); -} - -class _MobileBoardTrailingState extends State { - final TextEditingController _textController = TextEditingController(); - - bool isEditing = false; - - @override - void dispose() { - _textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; - final style = Theme.of(context); - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - child: SizedBox( - width: screenSize.width * 0.7, - child: isEditing - ? DecoratedBox( - decoration: BoxDecoration( - color: style.colorScheme.secondary, - borderRadius: BorderRadius.circular(8), - ), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: _textController, - autofocus: true, - onChanged: (_) => setState(() {}), - decoration: InputDecoration( - suffixIcon: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: _textController.text.isNotEmpty ? 1 : 0, - child: Material( - color: Colors.transparent, - shape: const CircleBorder(), - clipBehavior: Clip.antiAlias, - child: IconButton( - icon: Icon( - Icons.close, - color: style.colorScheme.onSurface, - ), - onPressed: () => - setState(() => _textController.clear()), - ), - ), - ), - isDense: true, - ), - onEditingComplete: () { - context.read().add( - BoardEvent.createGroup( - _textController.text, - ), - ); - _textController.clear(); - setState(() => isEditing = false); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton( - child: Text( - LocaleKeys.button_cancel.tr(), - style: style.textTheme.titleSmall?.copyWith( - color: style.colorScheme.onSurface, - ), - ), - onPressed: () => setState(() => isEditing = false), - ), - TextButton( - child: Text( - LocaleKeys.button_add.tr(), - style: style.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - color: style.colorScheme.onSurface, - ), - ), - onPressed: () { - context.read().add( - BoardEvent.createGroup( - _textController.text, - ), - ); - _textController.clear(); - setState(() => isEditing = false); - }, - ), - ], - ), - ], - ), - ), - ) - : ElevatedButton.icon( - style: ElevatedButton.styleFrom( - foregroundColor: style.colorScheme.onSurface, - backgroundColor: style.colorScheme.secondary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ).copyWith( - overlayColor: - WidgetStateProperty.all(Theme.of(context).hoverColor), - ), - icon: const Icon(Icons.add), - label: Text( - LocaleKeys.board_column_newGroup.tr(), - style: style.textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.w600, - ), - ), - onPressed: () => setState(() => isEditing = true), - ), - ), - ); - } -} 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 deleted file mode 100644 index f80525786e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart +++ /dev/null @@ -1,249 +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: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'; -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_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class MobileHiddenGroupsColumn extends StatelessWidget { - const MobileHiddenGroupsColumn({super.key, required this.padding}); - - final EdgeInsets padding; - - @override - Widget build(BuildContext context) { - final databaseController = context.read().databaseController; - return BlocSelector( - selector: (state) => state.maybeMap( - orElse: () => null, - ready: (value) => value.layoutSettings, - ), - builder: (context, layoutSettings) { - if (layoutSettings == null) { - return const SizedBox.shrink(); - } - final isCollapsed = layoutSettings.collapseHiddenGroups; - return Container( - padding: padding, - child: AnimatedSize( - alignment: AlignmentDirectional.topStart, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 150), - child: isCollapsed - ? SizedBox( - height: 50, - child: _collapseExpandIcon(context, isCollapsed), - ) - : SizedBox( - width: 180, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Spacer(), - _collapseExpandIcon(context, isCollapsed), - ], - ), - Text( - LocaleKeys.board_hiddenGroupSection_sectionTitle.tr(), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: Theme.of(context).colorScheme.tertiary, - ), - ), - const VSpace(8), - Expanded( - child: MobileHiddenGroupList( - databaseController: databaseController, - ), - ), - ], - ), - ), - ), - ); - }, - ); - } - - Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) { - return CircleAvatar( - radius: 20, - backgroundColor: Theme.of(context).colorScheme.secondary, - child: IconButton( - icon: FlowySvg( - isCollapsed - ? FlowySvgs.hamburger_s_s - : FlowySvgs.pull_left_outlined_s, - size: isCollapsed ? const Size.square(12) : const Size.square(40), - ), - onPressed: () => context - .read() - .add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)), - ), - ); - } -} - -class MobileHiddenGroupList extends StatelessWidget { - const MobileHiddenGroupList({ - super.key, - required this.databaseController, - }); - - final DatabaseController databaseController; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (_, state) { - return state.maybeMap( - orElse: () => const SizedBox.shrink(), - ready: (state) { - return ReorderableListView.builder( - itemCount: state.hiddenGroups.length, - itemBuilder: (_, index) => MobileHiddenGroup( - key: ValueKey(state.hiddenGroups[index].groupId), - group: state.hiddenGroups[index], - index: index, - ), - proxyDecorator: (child, index, animation) => BlocProvider.value( - value: context.read(), - child: Material(color: Colors.transparent, child: child), - ), - physics: const ClampingScrollPhysics(), - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex--; - } - final fromGroupId = state.hiddenGroups[oldIndex].groupId; - final toGroupId = state.hiddenGroups[newIndex].groupId; - context - .read() - .add(BoardEvent.reorderGroup(fromGroupId, toGroupId)); - }, - ); - }, - ); - }, - ); - } -} - -class MobileHiddenGroup extends StatelessWidget { - const MobileHiddenGroup({ - super.key, - required this.group, - required this.index, - }); - - final GroupPB group; - final int index; - - @override - Widget build(BuildContext context) { - final databaseController = context.read().databaseController; - final primaryField = databaseController.fieldController.fieldInfos - .firstWhereOrNull((element) => element.isPrimary)!; - - final cells = group.rows.map( - (item) { - final cellContext = - databaseController.rowCache.loadCells(item).firstWhere( - (cellContext) => cellContext.fieldId == primaryField.id, - ); - - return TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.bodyMedium, - foregroundColor: AFThemeExtension.of(context).onBackground, - visualDensity: VisualDensity.compact, - ), - child: CardCellBuilder( - databaseController: context.read().databaseController, - ).build( - cellContext: cellContext, - styleMap: {FieldType.RichText: _titleCellStyle(context)}, - hasNotes: !item.isDocumentEmpty, - ), - onPressed: () { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: item.id, - MobileRowDetailPage.argDatabaseController: - context.read().databaseController, - }, - ); - }, - ); - }, - ).toList(); - - return ExpansionTile( - tilePadding: EdgeInsets.zero, - childrenPadding: EdgeInsets.zero, - title: Row( - children: [ - Expanded( - child: Text( - group.generateGroupName(databaseController), - style: Theme.of(context).textTheme.bodyMedium, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - GestureDetector( - child: const Padding( - padding: EdgeInsets.all(4), - child: FlowySvg( - FlowySvgs.hide_m, - size: Size.square(20), - ), - ), - onTap: () => showFlowyMobileConfirmDialog( - context, - title: FlowyText(LocaleKeys.board_mobile_showGroup.tr()), - content: FlowyText( - LocaleKeys.board_mobile_showGroupContent.tr(), - ), - actionButtonTitle: LocaleKeys.button_yes.tr(), - actionButtonColor: Theme.of(context).colorScheme.primary, - onActionButtonPressed: () => context - .read() - .add(BoardEvent.setGroupVisibility(group, true)), - ), - ), - ], - ), - children: cells, - ); - } - - TextCardCellStyle _titleCellStyle(BuildContext context) { - return TextCardCellStyle( - padding: EdgeInsets.zero, - textStyle: Theme.of(context).textTheme.bodyMedium!, - maxLines: 2, - titleTextStyle: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 11, overflow: TextOverflow.ellipsis), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart deleted file mode 100644 index 0b1d45ab65..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'group_card_header.dart'; -export 'mobile_board_trailing.dart'; -export 'mobile_hidden_groups_column.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart deleted file mode 100644 index 64f41eceac..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'card_detail/mobile_card_detail_screen.dart'; -export 'mobile_card_content.dart'; 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 deleted file mode 100644 index 5896c51b9b..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ /dev/null @@ -1,578 +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.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'; -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/row/row_banner_bloc.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/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/grid/application/row/mobile_row_detail_bloc.dart'; -import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.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_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'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:go_router/go_router.dart'; - -import 'widgets/mobile_create_field_button.dart'; -import 'widgets/mobile_row_property_list.dart'; - -class MobileRowDetailPage extends StatefulWidget { - const MobileRowDetailPage({ - super.key, - required this.databaseController, - required this.rowId, - }); - - static const routeName = '/MobileRowDetailPage'; - static const argDatabaseController = 'databaseController'; - static const argRowId = 'rowId'; - - final DatabaseController databaseController; - final String rowId; - - @override - State createState() => _MobileRowDetailPageState(); -} - -class _MobileRowDetailPageState extends State { - late final MobileRowDetailBloc _bloc; - late final PageController _pageController; - - String get viewId => widget.databaseController.viewId; - - RowCache get rowCache => widget.databaseController.rowCache; - - FieldController get fieldController => - widget.databaseController.fieldController; - - @override - void initState() { - super.initState(); - _bloc = MobileRowDetailBloc( - databaseController: widget.databaseController, - )..add(MobileRowDetailEvent.initial(widget.rowId)); - final initialPage = rowCache.rowInfos - .indexWhere((rowInfo) => rowInfo.rowId == widget.rowId); - _pageController = - PageController(initialPage: initialPage == -1 ? 0 : initialPage); - } - - @override - void dispose() { - _bloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _bloc, - child: Scaffold( - appBar: FlowyAppBar( - leadingType: FlowyAppBarLeadingType.close, - showDivider: false, - actions: [ - AppBarMoreButton( - onTap: (_) => _showCardActions(context), - ), - ], - ), - body: BlocBuilder( - buildWhen: (previous, current) => - previous.rowInfos.length != current.rowInfos.length, - builder: (context, state) { - if (state.isLoading) { - return const SizedBox.shrink(); - } - return PageView.builder( - controller: _pageController, - onPageChanged: (page) { - final rowId = _bloc.state.rowInfos[page].rowId; - _bloc.add(MobileRowDetailEvent.changeRowId(rowId)); - }, - itemCount: state.rowInfos.length, - itemBuilder: (context, index) { - if (state.rowInfos.isEmpty || state.currentRowId == null) { - return const SizedBox.shrink(); - } - return MobileRowDetailPageContent( - databaseController: widget.databaseController, - rowMeta: state.rowInfos[index].rowMeta, - ); - }, - ); - }, - ), - floatingActionButton: RowDetailFab( - onTapPrevious: () => _pageController.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.ease, - ), - onTapNext: () => _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.ease, - ), - ), - ), - ); - } - - void _showCardActions(BuildContext context) { - showMobileBottomSheet( - context, - backgroundColor: AFThemeExtension.of(context).background, - showDragHandle: true, - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - MobileQuickActionButton( - onTap: () => - _performAction(viewId, _bloc.state.currentRowId, false), - 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(), - MobileQuickActionButton( - onTap: () => _performAction(viewId, _bloc.state.currentRowId, true), - text: LocaleKeys.button_delete.tr(), - textColor: Theme.of(context).colorScheme.error, - icon: FlowySvgs.trash_s, - iconColor: Theme.of(context).colorScheme.error, - ), - ], - ), - ); - } - - void _performAction(String viewId, String? rowId, bool deleteRow) { - if (rowId == null) { - return; - } - - deleteRow - ? RowBackendService.deleteRows(viewId, [rowId]) - : RowBackendService.duplicateRow(viewId, rowId); - - context - ..pop() - ..pop(); - Fluttertoast.showToast( - msg: deleteRow - ? LocaleKeys.board_cardDeleted.tr() - : LocaleKeys.board_cardDuplicated.tr(), - 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 { - const RowDetailFab({ - super.key, - required this.onTapPrevious, - required this.onTapNext, - }); - - final VoidCallback onTapPrevious; - final VoidCallback onTapNext; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final rowCount = state.rowInfos.length; - final rowIndex = state.rowInfos.indexWhere( - (rowInfo) => rowInfo.rowId == state.currentRowId, - ); - if (rowIndex == -1 || rowCount == 0) { - return const SizedBox.shrink(); - } - - final previousDisabled = rowIndex == 0; - final nextDisabled = rowIndex == rowCount - 1; - - return IntrinsicWidth( - child: Container( - height: 48, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(26), - boxShadow: const [ - BoxShadow( - offset: Offset(0, 8), - blurRadius: 20, - color: Color(0x191F2329), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox.square( - dimension: 48, - child: Material( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(26), - borderOnForeground: false, - child: previousDisabled - ? Icon( - Icons.chevron_left_outlined, - color: Theme.of(context).disabledColor, - ) - : InkWell( - borderRadius: BorderRadius.circular(26), - onTap: onTapPrevious, - child: const Icon(Icons.chevron_left_outlined), - ), - ), - ), - FlowyText.medium( - "${rowIndex + 1} / $rowCount", - fontSize: 14, - ), - SizedBox.square( - dimension: 48, - child: Material( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(26), - borderOnForeground: false, - child: nextDisabled - ? Icon( - Icons.chevron_right_outlined, - color: Theme.of(context).disabledColor, - ) - : InkWell( - borderRadius: BorderRadius.circular(26), - onTap: onTapNext, - child: const Icon(Icons.chevron_right_outlined), - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -class MobileRowDetailPageContent extends StatefulWidget { - const MobileRowDetailPageContent({ - super.key, - required this.databaseController, - required this.rowMeta, - }); - - final DatabaseController databaseController; - final RowMetaPB rowMeta; - - @override - State createState() => - MobileRowDetailPageContentState(); -} - -class MobileRowDetailPageContentState - extends State { - late final RowController rowController; - 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() { - super.initState(); - - rowController = RowController( - rowMeta: widget.rowMeta, - viewId: viewId, - rowCache: rowCache, - ); - rowController.initialize(); - - cellBuilder = EditableCellBuilder( - databaseController: widget.databaseController, - ); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => RowDetailBloc( - fieldController: fieldController, - 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, - ), - ], - ), - ), - child: SizedBox( - height: 200, - width: double.infinity, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - ), - child: AFImage( - url: rowDetailState.rowMeta.cover.data, - uploadType: widget.rowMeta.cover.uploadType, - userProfile: - context.read().userProfile, - ), - ), - ), - ), - ], - 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, - ), - ], - ), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class _TitleSkin extends IEditableTextCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TextCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 23, - fontWeight: FontWeight.w500, - ), - onEditingComplete: () { - bloc.add(TextCellEvent.updateText(textEditingController.text)); - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(vertical: 9), - 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, - ), - onTapOutside: (event) => focusNode.unfocus(), - ); - } -} 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 deleted file mode 100644 index 1d3d3efcf5..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.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/grid/presentation/layout/sizes.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileRowDetailCreateFieldButton extends StatelessWidget { - const MobileRowDetailCreateFieldButton({ - super.key, - required this.viewId, - required this.fieldController, - }); - - final String viewId; - final FieldController fieldController; - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints( - minWidth: double.infinity, - maxHeight: GridSize.headerHeight, - ), - 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, vertical: 2), - ), - ), - label: FlowyText.medium( - LocaleKeys.grid_field_newProperty.tr(), - fontSize: 15, - ), - onPressed: () => mobileCreateFieldWorkflow(context, viewId), - icon: const FlowySvg(FlowySvgs.add_m), - ), - ); - } -} 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 deleted file mode 100644 index 42bb241ab8..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart +++ /dev/null @@ -1,98 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileRowPropertyList extends StatelessWidget { - const MobileRowPropertyList({ - super.key, - required this.databaseController, - required this.cellBuilder, - }); - - final DatabaseController databaseController; - final EditableCellBuilder cellBuilder; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final List visibleCells = - state.visibleCells.where((cell) => !_isCellPrimary(cell)).toList(); - - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: visibleCells.length, - padding: EdgeInsets.zero, - itemBuilder: (context, index) => _PropertyCell( - key: ValueKey('row_detail_${visibleCells[index].fieldId}'), - cellContext: visibleCells[index], - fieldController: databaseController.fieldController, - cellBuilder: cellBuilder, - ), - separatorBuilder: (_, __) => const VSpace(22), - ); - }, - ); - } - - bool _isCellPrimary(CellContext cell) => - databaseController.fieldController.getField(cell.fieldId)!.isPrimary; -} - -class _PropertyCell extends StatefulWidget { - const _PropertyCell({ - super.key, - required this.cellContext, - required this.fieldController, - required this.cellBuilder, - }); - - final CellContext cellContext; - final FieldController fieldController; - final EditableCellBuilder cellBuilder; - - @override - State createState() => _PropertyCellState(); -} - -class _PropertyCellState extends State<_PropertyCell> { - @override - Widget build(BuildContext context) { - final fieldInfo = - widget.fieldController.getField(widget.cellContext.fieldId)!; - final cell = widget.cellBuilder - .buildStyled(widget.cellContext, EditableCellStyle.mobileRowDetail); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - FieldIcon( - fieldInfo: fieldInfo, - ), - const HSpace(6), - Expanded( - child: FlowyText.regular( - fieldInfo.name, - overflow: TextOverflow.ellipsis, - fontSize: 14, - figmaLineHeight: 16.0, - color: Theme.of(context).hintColor, - ), - ), - ], - ), - const VSpace(6), - cell, - ], - ); - } -} 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 deleted file mode 100644 index e2614296af..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class OptionTextField extends StatelessWidget { - const OptionTextField({ - super.key, - required this.controller, - this.autoFocus = false, - required this.isPrimary, - required this.fieldType, - required this.onTextChanged, - required this.onFieldTypeChanged, - }); - - final TextEditingController controller; - final bool autoFocus; - final bool isPrimary; - final FieldType fieldType; - final void Function(String value) onTextChanged; - final void Function(FieldType value) onFieldTypeChanged; - - @override - 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(), - ); - if (fieldType != null) { - onFieldTypeChanged(fieldType); - } - }, - child: Container( - height: 38, - width: 38, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Theme.of(context).brightness == Brightness.light - ? fieldType.mobileIconBackgroundColor - : fieldType.mobileIconBackgroundColorDark, - ), - child: Center( - child: FlowySvg( - fieldType.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/card_detail/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/widgets.dart deleted file mode 100644 index c972c3a331..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/widgets.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'mobile_create_field_button.dart'; -export 'mobile_row_property_list.dart'; -export 'option_text_field.dart'; 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 deleted file mode 100644 index aa9d23308a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart +++ /dev/null @@ -1,51 +0,0 @@ -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'; - -class MobileCardContent extends StatelessWidget { - const MobileCardContent({ - super.key, - required this.rowMeta, - 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, - ), - ), - ], - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index 8262cf6408..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; -import 'package:appflowy/plugins/base/drag_handler.dart'; -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/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/widgets/mobile_date_header.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileDateCellEditScreen extends StatefulWidget { - const MobileDateCellEditScreen({ - super.key, - required this.controller, - this.showAsFullScreen = true, - }); - - final DateCellController controller; - final bool showAsFullScreen; - - static const routeName = '/edit_date_cell'; - - // the type is DateCellController - static const dateCellController = 'date_cell_controller'; - - // bool value, default is true - static const fullScreen = 'full_screen'; - - @override - State createState() => - _MobileDateCellEditScreenState(); -} - -class _MobileDateCellEditScreenState extends State { - @override - Widget build(BuildContext context) => - widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen(); - - Widget _buildFullScreen() { - return Scaffold( - appBar: FlowyAppBar(titleText: LocaleKeys.titleBar_date.tr()), - body: _buildDatePicker(), - ); - } - - Widget _buildNotFullScreen() { - return DraggableScrollableSheet( - expand: false, - snap: true, - initialChildSize: 0.7, - minChildSize: 0.4, - snapSizes: const [0.4, 0.7, 1.0], - builder: (_, controller) => Material( - color: Colors.transparent, - child: ListView( - controller: controller, - children: [ - ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: const Center(child: DragHandle()), - ), - const MobileDateHeader(), - _buildDatePicker(), - ], - ), - ), - ); - } - - 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)); - }, - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 5abb2dc031..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_create_field_screen.dart +++ /dev/null @@ -1,93 +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/database/field/mobile_full_field_editor.dart'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.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:go_router/go_router.dart'; - -class MobileNewPropertyScreen extends StatefulWidget { - const MobileNewPropertyScreen({ - super.key, - required this.viewId, - this.fieldType, - }); - - final String viewId; - final FieldType? fieldType; - - static const routeName = '/new_property'; - static const argViewId = 'view_id'; - static const argFieldTypeId = 'field_type_id'; - - @override - State createState() => - _MobileNewPropertyScreenState(); -} - -class _MobileNewPropertyScreenState extends State { - late FieldOptionValues optionValues; - - @override - void initState() { - super.initState(); - - final type = widget.fieldType ?? FieldType.RichText; - optionValues = FieldOptionValues( - type: type, - icon: "", - name: type.i18n, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: FlowyAppBar( - centerTitle: true, - titleText: LocaleKeys.grid_field_newProperty.tr(), - leadingType: FlowyAppBarLeadingType.cancel, - actions: [ - _SaveButton( - onSave: () { - context.pop(optionValues); - }, - ), - ], - ), - body: MobileFieldEditor( - mode: FieldOptionMode.add, - defaultValues: optionValues, - onOptionValuesChanged: (optionValues) { - this.optionValues = optionValues; - }, - ), - ); - } -} - -class _SaveButton extends StatelessWidget { - const _SaveButton({ - required this.onSave, - }); - - final VoidCallback onSave; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Align( - child: GestureDetector( - onTap: onSave, - child: FlowyText.medium( - LocaleKeys.button_save.tr(), - color: const Color(0xFF00ADDC), - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index 75b52de414..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart +++ /dev/null @@ -1,119 +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/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/domain/field_service.dart'; -import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class MobileEditPropertyScreen extends StatefulWidget { - const MobileEditPropertyScreen({ - super.key, - required this.viewId, - required this.field, - }); - - final String viewId; - final FieldInfo field; - - static const routeName = '/edit_property'; - static const argViewId = 'view_id'; - static const argField = 'field'; - - @override - State createState() => - _MobileEditPropertyScreenState(); -} - -class _MobileEditPropertyScreenState extends State { - late final FieldBackendService fieldService; - late FieldOptionValues _fieldOptionValues; - - @override - void initState() { - super.initState(); - _fieldOptionValues = FieldOptionValues.fromField(field: widget.field.field); - fieldService = FieldBackendService( - viewId: widget.viewId, - fieldId: widget.field.id, - ); - } - - @override - Widget build(BuildContext context) { - final viewId = widget.viewId; - final fieldId = widget.field.id; - - return PopScope( - onPopInvokedWithResult: (didPop, _) { - if (!didPop) { - context.pop(_fieldOptionValues); - } - }, - child: Scaffold( - appBar: FlowyAppBar( - titleText: LocaleKeys.grid_field_editProperty.tr(), - onTapLeading: () => context.pop(_fieldOptionValues), - ), - body: MobileFieldEditor( - mode: FieldOptionMode.edit, - isPrimary: widget.field.isPrimary, - defaultValues: FieldOptionValues.fromField(field: widget.field.field), - actions: [ - widget.field.visibility?.isVisibleState() ?? true - ? FieldOptionAction.hide - : FieldOptionAction.show, - FieldOptionAction.duplicate, - FieldOptionAction.delete, - ], - onOptionValuesChanged: (fieldOptionValues) async { - await fieldService.updateField(name: fieldOptionValues.name); - - await FieldBackendService.updateFieldType( - viewId: widget.viewId, - fieldId: widget.field.id, - fieldType: fieldOptionValues.type, - ); - - final data = fieldOptionValues.getTypeOptionData(); - if (data != null) { - await FieldBackendService.updateFieldTypeOption( - viewId: widget.viewId, - fieldId: widget.field.id, - typeOptionData: data, - ); - } - setState(() { - _fieldOptionValues = fieldOptionValues; - }); - }, - onAction: (action) { - final service = FieldServices( - viewId: viewId, - fieldId: fieldId, - ); - switch (action) { - case FieldOptionAction.delete: - fieldService.delete(); - context.pop(); - return; - case FieldOptionAction.duplicate: - fieldService.duplicate(); - break; - case FieldOptionAction.hide: - service.hide(); - break; - case FieldOptionAction.show: - service.show(); - break; - } - 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 deleted file mode 100644 index 11a6b239e9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:flutter/material.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/database/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:go_router/go_router.dart'; - -import 'mobile_create_field_screen.dart'; -import 'mobile_edit_field_screen.dart'; -import 'mobile_field_picker_list.dart'; -import 'mobile_full_field_editor.dart'; -import 'mobile_quick_field_editor.dart'; - -const mobileSupportedFieldTypes = [ - FieldType.RichText, - FieldType.Number, - FieldType.SingleSelect, - FieldType.MultiSelect, - FieldType.DateTime, - FieldType.Media, - FieldType.URL, - FieldType.Checkbox, - FieldType.Checklist, - FieldType.LastEditedTime, - FieldType.CreatedTime, - // FieldType.Time, -]; - -Future showFieldTypeGridBottomSheet( - BuildContext context, { - required String title, -}) { - return showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showCloseButton: true, - elevation: 20, - title: title, - backgroundColor: AFThemeExtension.of(context).background, - enableDraggableScrollable: true, - builder: (context) { - final typeOptionMenuItemValue = mobileSupportedFieldTypes - .map( - (fieldType) => TypeOptionMenuItemValue( - value: fieldType, - backgroundColor: Theme.of(context).brightness == Brightness.light - ? fieldType.mobileIconBackgroundColor - : fieldType.mobileIconBackgroundColorDark, - text: fieldType.i18n, - icon: fieldType.svgData, - onTap: (context, fieldType) => - Navigator.of(context).pop(fieldType), - ), - ) - .toList(); - return Padding( - padding: EdgeInsets.all(16 * context.scale), - child: TypeOptionMenu( - values: typeOptionMenuItemValue, - scaleFactor: context.scale, - ), - ); - }, - ); -} - -/// Shows the field type grid and upon selection, allow users to edit the -/// field's properties and saving it when the user clicks save. -void mobileCreateFieldWorkflow( - BuildContext context, - String viewId, { - OrderObjectPositionPB? position, -}) async { - final fieldType = await showFieldTypeGridBottomSheet( - context, - title: LocaleKeys.grid_field_newProperty.tr(), - ); - if (fieldType == null || !context.mounted) { - return; - } - final optionValues = await context.push( - Uri( - path: MobileNewPropertyScreen.routeName, - queryParameters: { - MobileNewPropertyScreen.argViewId: viewId, - MobileNewPropertyScreen.argFieldTypeId: fieldType.value.toString(), - }, - ).toString(), - ); - if (optionValues != null) { - await optionValues.create(viewId: viewId, position: position); - } -} - -/// Used to edit a field. -Future showEditFieldScreen( - BuildContext context, - String viewId, - FieldInfo field, -) { - return context.push( - MobileEditPropertyScreen.routeName, - extra: { - MobileEditPropertyScreen.argViewId: viewId, - MobileEditPropertyScreen.argField: field, - }, - ); -} - -/// Shows some quick field options in a bottom sheet. -void showQuickEditField( - BuildContext context, - String viewId, - FieldController fieldController, - FieldInfo fieldInfo, -) { - showMobileBottomSheet( - context, - showDragHandle: true, - builder: (context) { - return SingleChildScrollView( - child: QuickEditField( - viewId: viewId, - fieldController: fieldController, - fieldInfo: fieldInfo, - ), - ); - }, - ); -} - -/// Display a list of fields in the current database that users can choose from. -Future showFieldPicker( - BuildContext context, - String title, - String? selectedFieldId, - FieldController fieldController, - bool Function(FieldInfo fieldInfo) filterBy, -) { - return showMobileBottomSheet( - context, - showDivider: false, - builder: (context) { - return MobileFieldPickerList( - title: title, - selectedFieldId: selectedFieldId, - fieldController: fieldController, - filterBy: filterBy, - ); - }, - ); -} 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 deleted file mode 100644 index 04144241a0..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_picker_list.dart +++ /dev/null @@ -1,142 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class MobileFieldPickerList extends StatefulWidget { - MobileFieldPickerList({ - super.key, - required this.title, - required this.selectedFieldId, - required FieldController fieldController, - required bool Function(FieldInfo fieldInfo) filterBy, - }) : fields = fieldController.fieldInfos.where(filterBy).toList(); - - final String title; - final String? selectedFieldId; - final List fields; - - @override - State createState() => _MobileFieldPickerListState(); -} - -class _MobileFieldPickerListState extends State { - String? newFieldId; - - @override - void initState() { - super.initState(); - newFieldId = widget.selectedFieldId; - } - - @override - Widget build(BuildContext context) { - return DraggableScrollableSheet( - expand: false, - snap: true, - initialChildSize: 0.98, - minChildSize: 0.98, - maxChildSize: 0.98, - builder: (context, scrollController) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DragHandle(), - _Header( - title: widget.title, - onDone: (context) => context.pop(newFieldId), - ), - SingleChildScrollView( - controller: scrollController, - child: ListView.builder( - shrinkWrap: true, - itemCount: widget.fields.length, - itemBuilder: (context, index) => _FieldButton( - field: widget.fields[index], - showTopBorder: index == 0, - isSelected: widget.fields[index].id == newFieldId, - onSelect: (fieldId) => setState(() => newFieldId = fieldId), - ), - ), - ), - ], - ); - }, - ); - } -} - -/// Same header as the one in showMobileBottomSheet, but allows popping the -/// sheet with a value. -class _Header extends StatelessWidget { - const _Header({ - required this.title, - required this.onDone, - }); - - final String title; - final void Function(BuildContext context) onDone; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: SizedBox( - height: 44.0, - child: Stack( - children: [ - const Align( - alignment: Alignment.centerLeft, - child: AppBarBackButton(), - ), - Align( - child: FlowyText.medium( - title, - fontSize: 16.0, - ), - ), - Align( - alignment: Alignment.centerRight, - child: AppBarDoneButton( - onTap: () => onDone(context), - ), - ), - ], - ), - ), - ); - } -} - -class _FieldButton extends StatelessWidget { - const _FieldButton({ - required this.field, - required this.isSelected, - required this.onSelect, - required this.showTopBorder, - }); - - final FieldInfo field; - final bool isSelected; - final void Function(String fieldId) onSelect; - final bool showTopBorder; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.checkbox( - text: field.name, - isSelected: isSelected, - leftIcon: FieldIcon( - fieldInfo: field, - dimension: 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 deleted file mode 100644 index f3d71a7f0e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart +++ /dev/null @@ -1,997 +0,0 @@ -import 'dart:math'; -import 'dart:typed_data'; - -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/base/option_color_list.dart'; -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/widgets/field/type_option_editor/date/date_time_format.dart'; -import 'package:appflowy/util/field_type_extension.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/uuid.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:protobuf/protobuf.dart'; - -import 'mobile_field_bottom_sheets.dart'; - -enum FieldOptionMode { - add, - edit, -} - -class FieldOptionValues { - FieldOptionValues({ - required this.type, - required this.name, - required this.icon, - this.dateFormat, - this.timeFormat, - this.includeTime, - this.numberFormat, - this.selectOption = const [], - }); - - factory FieldOptionValues.fromField({required FieldPB field}) { - final fieldType = field.fieldType; - final buffer = field.typeOptionData; - return FieldOptionValues( - type: fieldType, - name: field.name, - icon: field.icon, - numberFormat: fieldType == FieldType.Number - ? NumberTypeOptionPB.fromBuffer(buffer).format - : null, - dateFormat: switch (fieldType) { - FieldType.DateTime => DateTypeOptionPB.fromBuffer(buffer).dateFormat, - FieldType.LastEditedTime || - FieldType.CreatedTime => - TimestampTypeOptionPB.fromBuffer(buffer).dateFormat, - _ => null - }, - timeFormat: switch (fieldType) { - FieldType.DateTime => DateTypeOptionPB.fromBuffer(buffer).timeFormat, - FieldType.LastEditedTime || - FieldType.CreatedTime => - TimestampTypeOptionPB.fromBuffer(buffer).timeFormat, - _ => null - }, - includeTime: switch (fieldType) { - FieldType.LastEditedTime || - FieldType.CreatedTime => - TimestampTypeOptionPB.fromBuffer(buffer).includeTime, - _ => null - }, - selectOption: switch (fieldType) { - FieldType.SingleSelect => - SingleSelectTypeOptionPB.fromBuffer(buffer).options, - FieldType.MultiSelect => - MultiSelectTypeOptionPB.fromBuffer(buffer).options, - _ => [], - }, - ); - } - - FieldType type; - String name; - String icon; - - // FieldType.DateTime - // FieldType.LastEditedTime - // FieldType.CreatedTime - DateFormatPB? dateFormat; - TimeFormatPB? timeFormat; - - // FieldType.LastEditedTime - // FieldType.CreatedTime - bool? includeTime; - - // FieldType.Number - NumberFormatPB? numberFormat; - - // FieldType.Select - // FieldType.MultiSelect - List selectOption; - - Future create({ - required String viewId, - OrderObjectPositionPB? position, - }) async { - await FieldBackendService.createField( - viewId: viewId, - fieldType: type, - fieldName: name, - typeOptionData: getTypeOptionData(), - position: position, - ); - } - - Uint8List? getTypeOptionData() { - switch (type) { - case FieldType.RichText: - case FieldType.URL: - case FieldType.Checkbox: - case FieldType.Time: - return null; - case FieldType.Number: - return NumberTypeOptionPB( - format: numberFormat, - ).writeToBuffer(); - case FieldType.DateTime: - return DateTypeOptionPB( - dateFormat: dateFormat, - timeFormat: timeFormat, - ).writeToBuffer(); - case FieldType.SingleSelect: - return SingleSelectTypeOptionPB( - options: selectOption, - ).writeToBuffer(); - case FieldType.MultiSelect: - return MultiSelectTypeOptionPB( - options: selectOption, - ).writeToBuffer(); - case FieldType.Checklist: - return ChecklistTypeOptionPB().writeToBuffer(); - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - return TimestampTypeOptionPB( - dateFormat: dateFormat, - timeFormat: timeFormat, - includeTime: includeTime, - ).writeToBuffer(); - case FieldType.Media: - return MediaTypeOptionPB().writeToBuffer(); - default: - throw UnimplementedError(); - } - } -} - -enum FieldOptionAction { - hide, - show, - duplicate, - delete, -} - -class MobileFieldEditor extends StatefulWidget { - const MobileFieldEditor({ - super.key, - required this.mode, - required this.defaultValues, - required this.onOptionValuesChanged, - this.actions = const [], - this.onAction, - this.isPrimary = false, - }); - - final FieldOptionMode mode; - final FieldOptionValues defaultValues; - final void Function(FieldOptionValues values) onOptionValuesChanged; - - // only used in edit mode - final List actions; - final void Function(FieldOptionAction action)? onAction; - - // the primary field can't be deleted, duplicated, and changed type - final bool isPrimary; - - @override - State createState() => _MobileFieldEditorState(); -} - -class _MobileFieldEditorState extends State { - final controller = TextEditingController(); - bool isFieldNameChanged = false; - - late FieldOptionValues values; - - @override - void initState() { - super.initState(); - values = widget.defaultValues; - controller.text = values.name; - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final option = _buildOption(); - return Container( - color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFFF7F8FB) - : const Color(0xFF23262B), - height: MediaQuery.of(context).size.height, - child: SingleChildScrollView( - child: Column( - children: [ - const _Divider(), - OptionTextField( - controller: controller, - autoFocus: widget.mode == FieldOptionMode.add, - fieldType: values.type, - isPrimary: widget.isPrimary, - onTextChanged: (value) { - isFieldNameChanged = true; - _updateOptionValues(name: value); - }, - onFieldTypeChanged: (type) { - setState( - () { - if (widget.mode == FieldOptionMode.add && - !isFieldNameChanged) { - controller.text = type.i18n; - _updateOptionValues(name: type.i18n); - } - _updateOptionValues(type: type); - }, - ); - }, - ), - const _Divider(), - if (!widget.isPrimary) ...[ - _PropertyType( - type: values.type, - onSelected: (type) { - setState( - () { - if (widget.mode == FieldOptionMode.add && - !isFieldNameChanged) { - controller.text = type.i18n; - _updateOptionValues(name: type.i18n); - } - _updateOptionValues(type: type); - }, - ); - }, - ), - const _Divider(), - if (option.isNotEmpty) ...[ - ...option, - const _Divider(), - ], - ], - ..._buildOptionActions(), - const _Divider(), - VSpace(MediaQuery.viewPaddingOf(context).bottom == 0 ? 28.0 : 16.0), - ], - ), - ), - ); - } - - List _buildOption() { - switch (values.type) { - case FieldType.Number: - return [ - _NumberOption( - selectedFormat: values.numberFormat ?? NumberFormatPB.Num, - onSelected: (format) => setState( - () => _updateOptionValues( - numberFormat: format, - ), - ), - ), - ]; - case FieldType.DateTime: - return [ - _DateOption( - selectedFormat: values.dateFormat ?? DateFormatPB.Local, - onSelected: (format) => _updateOptionValues( - dateFormat: format, - ), - ), - const _Divider(), - _TimeOption( - selectedFormat: values.timeFormat ?? TimeFormatPB.TwelveHour, - onSelected: (format) => _updateOptionValues( - timeFormat: format, - ), - ), - ]; - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - return [ - _DateOption( - selectedFormat: values.dateFormat ?? DateFormatPB.Local, - onSelected: (format) => _updateOptionValues( - dateFormat: format, - ), - ), - const _Divider(), - _TimeOption( - selectedFormat: values.timeFormat ?? TimeFormatPB.TwelveHour, - onSelected: (format) => _updateOptionValues( - timeFormat: format, - ), - ), - const _Divider(), - _IncludeTimeOption( - includeTime: values.includeTime ?? true, - onToggle: (includeTime) => _updateOptionValues( - includeTime: includeTime, - ), - ), - ]; - case FieldType.SingleSelect: - case FieldType.MultiSelect: - return [ - _SelectOption( - mode: widget.mode, - selectOption: values.selectOption, - onAddOptions: (options) { - if (values.selectOption.lastOrNull?.name.isEmpty == true) { - // ignore the add action if the last one doesn't have a name - return; - } - setState(() { - _updateOptionValues( - selectOption: values.selectOption + options, - ); - }); - }, - onUpdateOptions: (options) { - _updateOptionValues(selectOption: options); - }, - ), - ]; - default: - return []; - } - } - - List _buildOptionActions() { - if (widget.mode == FieldOptionMode.add || widget.actions.isEmpty) { - return []; - } - - return [ - if (widget.actions.contains(FieldOptionAction.hide) && !widget.isPrimary) - FlowyOptionTile.text( - text: LocaleKeys.grid_field_hide.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s), - onTap: () => widget.onAction?.call(FieldOptionAction.hide), - ), - if (widget.actions.contains(FieldOptionAction.show)) - FlowyOptionTile.text( - text: LocaleKeys.grid_field_show.tr(), - leftIcon: const FlowySvg(FlowySvgs.show_m, size: Size.square(16)), - onTap: () => widget.onAction?.call(FieldOptionAction.show), - ), - if (widget.actions.contains(FieldOptionAction.duplicate) && - !widget.isPrimary) - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.button_duplicate.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s), - onTap: () => widget.onAction?.call(FieldOptionAction.duplicate), - ), - if (widget.actions.contains(FieldOptionAction.delete) && - !widget.isPrimary) - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.button_delete.tr(), - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.m_delete_s, - color: Theme.of(context).colorScheme.error, - ), - onTap: () => widget.onAction?.call(FieldOptionAction.delete), - ), - ]; - } - - void _updateOptionValues({ - FieldType? type, - String? name, - DateFormatPB? dateFormat, - TimeFormatPB? timeFormat, - bool? includeTime, - NumberFormatPB? numberFormat, - List? selectOption, - }) { - if (type != null) { - values.type = type; - } - if (name != null) { - values.name = name; - } - if (dateFormat != null) { - values.dateFormat = dateFormat; - } - if (timeFormat != null) { - values.timeFormat = timeFormat; - } - if (includeTime != null) { - values.includeTime = includeTime; - } - if (numberFormat != null) { - values.numberFormat = numberFormat; - } - if (selectOption != null) { - values.selectOption = selectOption; - } - - widget.onOptionValuesChanged(values); - } -} - -class _PropertyType extends StatelessWidget { - const _PropertyType({ - required this.type, - required this.onSelected, - }); - - final FieldType type; - final void Function(FieldType type) onSelected; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.text( - text: LocaleKeys.grid_field_propertyType.tr(), - trailing: Row( - children: [ - FlowySvg( - type.svgData, - size: const Size.square(22), - color: Theme.of(context).hintColor, - ), - const HSpace(6.0), - FlowyText( - type.i18n, - 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: () async { - final fieldType = await showFieldTypeGridBottomSheet( - context, - title: LocaleKeys.grid_field_editProperty.tr(), - ); - if (fieldType != null) { - onSelected(fieldType); - } - }, - ); - } -} - -class _Divider extends StatelessWidget { - const _Divider(); - - @override - Widget build(BuildContext context) { - return const VSpace( - 24.0, - ); - } -} - -class _DateOption extends StatefulWidget { - const _DateOption({ - required this.selectedFormat, - required this.onSelected, - }); - - final DateFormatPB selectedFormat; - final Function(DateFormatPB format) onSelected; - - @override - State<_DateOption> createState() => _DateOptionState(); -} - -class _DateOptionState extends State<_DateOption> { - DateFormatPB selectedFormat = DateFormatPB.Local; - - @override - void initState() { - super.initState(); - - selectedFormat = widget.selectedFormat; - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), - child: FlowyText( - LocaleKeys.grid_field_dateFormat.tr().toUpperCase(), - fontSize: 13, - color: Theme.of(context).hintColor, - ), - ), - ...DateFormatPB.values.mapIndexed((index, format) { - return FlowyOptionTile.checkbox( - text: format.title(), - isSelected: selectedFormat == format, - showTopBorder: index == 0, - onTap: () { - widget.onSelected(format); - setState(() { - selectedFormat = format; - }); - }, - ); - }), - ], - ); - } -} - -class _TimeOption extends StatefulWidget { - const _TimeOption({ - required this.selectedFormat, - required this.onSelected, - }); - - final TimeFormatPB selectedFormat; - final Function(TimeFormatPB format) onSelected; - - @override - State<_TimeOption> createState() => _TimeOptionState(); -} - -class _TimeOptionState extends State<_TimeOption> { - TimeFormatPB selectedFormat = TimeFormatPB.TwelveHour; - - @override - void initState() { - super.initState(); - - selectedFormat = widget.selectedFormat; - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), - child: FlowyText( - LocaleKeys.grid_field_timeFormat.tr().toUpperCase(), - fontSize: 13, - color: Theme.of(context).hintColor, - ), - ), - ...TimeFormatPB.values.mapIndexed((index, format) { - return FlowyOptionTile.checkbox( - text: format.title(), - isSelected: selectedFormat == format, - showTopBorder: index == 0, - onTap: () { - widget.onSelected(format); - setState(() { - selectedFormat = format; - }); - }, - ); - }), - ], - ); - } -} - -class _IncludeTimeOption extends StatefulWidget { - const _IncludeTimeOption({ - required this.includeTime, - required this.onToggle, - }); - - final bool includeTime; - final void Function(bool includeTime) onToggle; - - @override - State<_IncludeTimeOption> createState() => _IncludeTimeOptionState(); -} - -class _IncludeTimeOptionState extends State<_IncludeTimeOption> { - late bool includeTime = widget.includeTime; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.toggle( - text: LocaleKeys.grid_field_includeTime.tr(), - isSelected: includeTime, - onValueChanged: (value) { - widget.onToggle(value); - setState(() { - includeTime = value; - }); - }, - ); - } -} - -class _NumberOption extends StatelessWidget { - const _NumberOption({ - required this.selectedFormat, - required this.onSelected, - }); - - final NumberFormatPB selectedFormat; - final void Function(NumberFormatPB format) onSelected; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.text( - text: LocaleKeys.grid_field_numberFormat.tr(), - trailing: Row( - children: [ - FlowyText( - selectedFormat.title(), - 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, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (context) { - return DraggableScrollableSheet( - expand: false, - snap: true, - minChildSize: 0.5, - builder: (context, scrollController) => _NumberFormatList( - scrollController: scrollController, - selectedFormat: selectedFormat, - onSelected: (type) { - onSelected(type); - context.pop(); - }, - ), - ); - }, - ); - }, - ); - } -} - -class _NumberFormatList extends StatefulWidget { - const _NumberFormatList({ - this.scrollController, - required this.selectedFormat, - required this.onSelected, - }); - - final NumberFormatPB selectedFormat; - final ScrollController? scrollController; - final void Function(NumberFormatPB format) onSelected; - - @override - State<_NumberFormatList> createState() => _NumberFormatListState(); -} - -class _NumberFormatListState extends State<_NumberFormatList> { - List formats = NumberFormatPB.values; - - @override - Widget build(BuildContext context) { - return ListView( - controller: widget.scrollController, - children: [ - const Center( - child: DragHandle(), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - height: 44.0, - child: FlowySearchTextField( - onChanged: (String value) { - setState(() { - formats = NumberFormatPB.values - .where( - (element) => element - .title() - .toLowerCase() - .contains(value.toLowerCase()), - ) - .toList(); - }); - }, - ), - ), - ...formats.mapIndexed( - (index, element) => FlowyOptionTile.checkbox( - text: element.title(), - content: Expanded( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - 4.0, - 16.0, - 12.0, - 16.0, - ), - child: FlowyText( - element.title(), - fontSize: 16, - ), - ), - FlowyText( - element.iconSymbol(), - fontSize: 16, - color: Theme.of(context).hintColor, - ), - widget.selectedFormat != element - ? const HSpace(30.0) - : const HSpace(6.0), - ], - ), - ), - isSelected: widget.selectedFormat == element, - showTopBorder: false, - onTap: () => widget.onSelected(element), - ), - ), - ], - ); - } -} - -// single select or multi select -class _SelectOption extends StatelessWidget { - _SelectOption({ - required this.mode, - required this.selectOption, - required this.onAddOptions, - required this.onUpdateOptions, - }); - - final List selectOption; - final void Function(List options) onAddOptions; - final void Function(List options) onUpdateOptions; - final FieldOptionMode mode; - - final random = Random(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), - child: FlowyText( - LocaleKeys.grid_field_optionTitle.tr().toUpperCase(), - fontSize: 13, - color: Theme.of(context).hintColor, - ), - ), - _SelectOptionList( - selectOptions: selectOption, - onUpdateOptions: onUpdateOptions, - ), - FlowyOptionTile.text( - text: LocaleKeys.grid_field_addOption.tr(), - leftIcon: const FlowySvg( - FlowySvgs.add_s, - size: Size.square(20), - ), - onTap: () { - onAddOptions([ - SelectOptionPB( - id: uuid(), - name: '', - color: SelectOptionColorPB.valueOf( - random.nextInt(SelectOptionColorPB.values.length), - ), - ), - ]); - }, - ), - ], - ); - } -} - -class _SelectOptionList extends StatefulWidget { - const _SelectOptionList({ - required this.selectOptions, - required this.onUpdateOptions, - }); - - final List selectOptions; - final void Function(List options) onUpdateOptions; - - @override - State<_SelectOptionList> createState() => _SelectOptionListState(); -} - -class _SelectOptionListState extends State<_SelectOptionList> { - late List options; - - @override - void initState() { - super.initState(); - - options = widget.selectOptions; - } - - @override - void didUpdateWidget(covariant _SelectOptionList oldWidget) { - super.didUpdateWidget(oldWidget); - - options = widget.selectOptions; - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (widget.selectOptions.isEmpty) { - return const SizedBox.shrink(); - } - 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( - option: option, - showTopBorder: index == 0, - showBottomBorder: index != widget.selectOptions.length - 1, - onUpdateOption: (option) { - _updateOption(index, option); - }, - ), - ) - .toList(), - ); - } - - void _updateOption(int index, SelectOptionPB option) { - final options = [...this.options]; - options[index] = option; - this.options = options; - widget.onUpdateOptions(options); - } -} - -class _SelectOptionTile extends StatefulWidget { - const _SelectOptionTile({ - required this.option, - required this.showTopBorder, - required this.showBottomBorder, - required this.onUpdateOption, - }); - - final SelectOptionPB option; - final bool showTopBorder; - final bool showBottomBorder; - final void Function(SelectOptionPB option) onUpdateOption; - - @override - State<_SelectOptionTile> createState() => _SelectOptionTileState(); -} - -class _SelectOptionTileState extends State<_SelectOptionTile> { - final TextEditingController controller = TextEditingController(); - late SelectOptionPB option; - - @override - void initState() { - super.initState(); - - controller.text = widget.option.name; - option = widget.option; - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.textField( - controller: controller, - textFieldHintText: LocaleKeys.grid_field_typeANewOption.tr(), - showTopBorder: widget.showTopBorder, - showBottomBorder: widget.showBottomBorder, - trailing: _SelectOptionColor( - color: option.color, - onChanged: (color) { - setState(() { - option.freeze(); - option = option.rebuild((p0) => p0.color = color); - widget.onUpdateOption(option); - }); - context.pop(); - }, - ), - onTextChanged: (name) { - setState(() { - option.freeze(); - option = option.rebuild((p0) => p0.name = name); - widget.onUpdateOption(option); - }); - }, - ); - } -} - -class _SelectOptionColor extends StatelessWidget { - const _SelectOptionColor({ - required this.color, - required this.onChanged, - }); - - final SelectOptionColorPB color; - final void Function(SelectOptionColorPB) onChanged; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - showCloseButton: true, - title: LocaleKeys.grid_selectOption_colorPanelTitle.tr(), - padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), - builder: (context) { - return OptionColorList( - selectedColor: color, - onSelectedColor: onChanged, - ); - }, - ); - }, - child: Container( - decoration: BoxDecoration( - color: color.toColor(context), - borderRadius: Corners.s10Border, - ), - width: 32, - height: 32, - alignment: Alignment.center, - child: const FlowySvg( - FlowySvgs.arrow_down_s, - size: Size.square(20), - ), - ), - ); - } -} 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 deleted file mode 100644 index f2b90e9c0d..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart +++ /dev/null @@ -1,193 +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:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart'; -import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.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_backend_service.dart'; -import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.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_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class QuickEditField extends StatefulWidget { - const QuickEditField({ - super.key, - required this.viewId, - required this.fieldController, - required this.fieldInfo, - }); - - final String viewId; - final FieldController fieldController; - final FieldInfo fieldInfo; - - @override - State createState() => _QuickEditFieldState(); -} - -class _QuickEditFieldState extends State { - final TextEditingController controller = TextEditingController(); - - late final FieldServices service = FieldServices( - viewId: widget.viewId, - fieldId: widget.fieldInfo.field.id, - ); - - late FieldVisibility fieldVisibility; - - @override - void initState() { - super.initState(); - fieldVisibility = - widget.fieldInfo.visibility ?? FieldVisibility.AlwaysShown; - controller.text = widget.fieldInfo.field.name; - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => FieldEditorBloc( - viewId: widget.viewId, - fieldController: widget.fieldController, - fieldInfo: widget.fieldInfo, - isNew: false, - ), - child: BlocConsumer( - listenWhen: (previous, current) => - previous.field.name != current.field.name, - listener: (context, state) => controller.text = state.field.name, - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(16), - OptionTextField( - controller: controller, - isPrimary: state.field.isPrimary, - fieldType: state.field.fieldType, - onTextChanged: (text) { - context - .read() - .add(FieldEditorEvent.renameField(text)); - }, - onFieldTypeChanged: (fieldType) { - context - .read() - .add(FieldEditorEvent.switchFieldType(fieldType)); - }, - ), - const _Divider(), - FlowyOptionTile.text( - text: LocaleKeys.grid_field_editProperty.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_field_edit_s), - onTap: () { - showEditFieldScreen( - context, - widget.viewId, - state.field, - ); - context.pop(); - }, - ), - if (!widget.fieldInfo.isPrimary) ...[ - FlowyOptionTile.text( - showTopBorder: false, - text: fieldVisibility.isVisibleState() - ? LocaleKeys.grid_field_hide.tr() - : LocaleKeys.grid_field_show.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s), - onTap: () async { - context.pop(); - if (fieldVisibility.isVisibleState()) { - await service.hide(); - } else { - await service.hide(); - } - }, - ), - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.grid_field_insertLeft.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_left_s), - onTap: () { - context.pop(); - mobileCreateFieldWorkflow( - context, - widget.viewId, - position: OrderObjectPositionPB( - position: OrderObjectPositionTypePB.Before, - objectId: widget.fieldInfo.id, - ), - ); - }, - ), - ], - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.grid_field_insertRight.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_right_s), - onTap: () { - context.pop(); - mobileCreateFieldWorkflow( - context, - widget.viewId, - position: OrderObjectPositionPB( - position: OrderObjectPositionTypePB.After, - objectId: widget.fieldInfo.id, - ), - ); - }, - ), - if (!widget.fieldInfo.isPrimary) ...[ - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.button_duplicate.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s), - onTap: () { - context.pop(); - service.duplicate(); - }, - ), - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.button_delete.tr(), - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.m_field_delete_s, - color: Theme.of(context).colorScheme.error, - ), - onTap: () { - context.pop(); - service.delete(); - }, - ), - ], - ], - ); - }, - ), - ); - } -} - -class _Divider extends StatelessWidget { - const _Divider(); - - @override - Widget build(BuildContext context) { - return const VSpace(20); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_empty.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_empty.dart deleted file mode 100644 index 29fcb6bdde..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_empty.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.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'; - -class MobileCalendarEventsEmpty extends StatelessWidget { - const MobileCalendarEventsEmpty({super.key}); - - @override - Widget build(BuildContext context) { - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - LocaleKeys.calendar_mobileEventScreen_emptyTitle.tr(), - fontWeight: FontWeight.w700, - fontSize: 14, - ), - const VSpace(8), - FlowyText.regular( - LocaleKeys.calendar_mobileEventScreen_emptyBody.tr(), - textAlign: TextAlign.center, - maxLines: 2, - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_screen.dart deleted file mode 100644 index 53889be311..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_screen.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; -import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_empty.dart'; -import 'package:appflowy/plugins/database/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; -import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_card.dart'; -import 'package:calendar_view/calendar_view.dart'; -import 'package:collection/collection.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 MobileCalendarEventsScreen extends StatefulWidget { - const MobileCalendarEventsScreen({ - super.key, - required this.calendarBloc, - required this.date, - required this.events, - required this.rowCache, - required this.viewId, - }); - - final CalendarBloc calendarBloc; - final DateTime date; - final List events; - final RowCache rowCache; - final String viewId; - - static const routeName = '/calendar_events'; - - // GoRouter Arguments - static const calendarBlocKey = 'calendar_bloc'; - static const calendarDateKey = 'date'; - static const calendarEventsKey = 'events'; - static const calendarRowCacheKey = 'row_cache'; - static const calendarViewIdKey = 'view_id'; - - @override - State createState() => - _MobileCalendarEventsScreenState(); -} - -class _MobileCalendarEventsScreenState - extends State { - late final List _events = widget.events; - - @override - Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: FloatingActionButton( - key: const Key('add_event_fab'), - elevation: 6, - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Theme.of(context).colorScheme.onPrimary, - onPressed: () => - widget.calendarBloc.add(CalendarEvent.createEvent(widget.date)), - child: const Text('+'), - ), - appBar: FlowyAppBar( - titleText: DateFormat.yMMMMd(context.locale.toLanguageTag()) - .format(widget.date), - ), - body: BlocProvider.value( - value: widget.calendarBloc, - child: BlocBuilder( - buildWhen: (p, c) => - p.newEvent != c.newEvent && - c.newEvent?.date.withoutTime == widget.date, - builder: (context, state) { - if (state.newEvent?.event != null && - _events - .none((e) => e.eventId == state.newEvent!.event!.eventId) && - state.newEvent!.date.withoutTime == widget.date) { - _events.add(state.newEvent!.event!); - } - - if (_events.isEmpty) { - return const MobileCalendarEventsEmpty(); - } - - return SingleChildScrollView( - child: Column( - children: [ - const VSpace(10), - ..._events.map((event) { - return EventCard( - databaseController: - widget.calendarBloc.databaseController, - event: event, - constraints: const BoxConstraints.expand(), - autoEdit: false, - isDraggable: false, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 3, - ), - ); - }), - const VSpace(24), - ], - ), - ); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart deleted file mode 100644 index ec87b5c9bd..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - -class MobileCalendarScreen extends StatelessWidget { - const MobileCalendarScreen({ - super.key, - required this.id, - this.title, - }); - - /// view id - final String id; - final String? title; - - static const routeName = '/calendar'; - static const viewId = 'id'; - static const viewTitle = 'title'; - - @override - Widget build(BuildContext context) { - return MobileViewPage( - id: id, - title: title, - viewLayout: ViewLayoutPB.Document, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart deleted file mode 100644 index 18c1f8f4d9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; - -class MobileGridScreen extends StatelessWidget { - const MobileGridScreen({ - super.key, - required this.id, - this.title, - this.arguments, - }); - - /// view id - final String id; - final String? title; - final Map? arguments; - - static const routeName = '/grid'; - static const viewId = 'id'; - static const viewTitle = 'title'; - static const viewArgs = 'arguments'; - - @override - Widget build(BuildContext context) { - return MobileViewPage( - id: id, - title: title, - viewLayout: ViewLayoutPB.Grid, - arguments: arguments, - ); - } -} 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 deleted file mode 100644 index 9543a4593b..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart +++ /dev/null @@ -1,205 +0,0 @@ -import 'dart:ui'; - -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_option_tile.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/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/workspace/application/view/view_bloc.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../field/mobile_field_bottom_sheets.dart'; - -class MobileDatabaseFieldList extends StatelessWidget { - const MobileDatabaseFieldList({ - super.key, - required this.databaseController, - required this.canCreate, - }); - - final DatabaseController databaseController; - final bool canCreate; - - @override - Widget build(BuildContext context) { - return _MobileDatabaseFieldListBody( - databaseController: databaseController, - viewId: context.read().state.view.id, - canCreate: canCreate, - ); - } -} - -class _MobileDatabaseFieldListBody extends StatelessWidget { - const _MobileDatabaseFieldListBody({ - required this.databaseController, - required this.viewId, - required this.canCreate, - }); - - final DatabaseController databaseController; - final String viewId; - final bool canCreate; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => DatabasePropertyBloc( - viewId: viewId, - fieldController: databaseController.fieldController, - )..add(const DatabasePropertyEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - if (state.fieldContexts.isEmpty) { - return const SizedBox.shrink(); - } - - final fields = [...state.fieldContexts]; - final firstField = fields.removeAt(0); - final firstCell = DatabaseFieldListTile( - key: ValueKey(firstField.id), - viewId: viewId, - fieldController: databaseController.fieldController, - fieldInfo: firstField, - showTopBorder: false, - ); - final cells = fields - .mapIndexed( - (index, field) => DatabaseFieldListTile( - key: ValueKey(field.id), - viewId: viewId, - fieldController: databaseController.fieldController, - fieldInfo: field, - showTopBorder: false, - ), - ) - .toList(); - - return ReorderableListView.builder( - proxyDecorator: (_, index, anim) { - final field = fields[index]; - return AnimatedBuilder( - animation: anim, - builder: (BuildContext context, Widget? child) { - final double animValue = - Curves.easeInOut.transform(anim.value); - final double scale = lerpDouble(1, 1.05, animValue)!; - return Transform.scale( - scale: scale, - child: Material( - child: DatabaseFieldListTile( - key: ValueKey(field.id), - viewId: viewId, - fieldController: databaseController.fieldController, - fieldInfo: field, - showTopBorder: true, - ), - ), - ); - }, - ); - }, - shrinkWrap: true, - onReorder: (from, to) { - from++; - to++; - context - .read() - .add(DatabasePropertyEvent.moveField(from, to)); - }, - header: firstCell, - footer: canCreate - ? Padding( - padding: const EdgeInsets.only(top: 20), - child: _NewDatabaseFieldTile(viewId: viewId), - ) - : null, - itemCount: cells.length, - itemBuilder: (context, index) => cells[index], - padding: EdgeInsets.only( - bottom: context.bottomSheetPadding(ignoreViewPadding: false), - ), - ); - }, - ), - ); - } -} - -class DatabaseFieldListTile extends StatelessWidget { - const DatabaseFieldListTile({ - super.key, - required this.fieldInfo, - required this.viewId, - required this.fieldController, - required this.showTopBorder, - }); - - final FieldInfo fieldInfo; - final String viewId; - final FieldController fieldController; - final bool showTopBorder; - - @override - Widget build(BuildContext context) { - if (fieldInfo.field.isPrimary) { - return FlowyOptionTile.text( - text: fieldInfo.name, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - dimension: 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, - ), - showTopBorder: showTopBorder, - onTap: () => showEditFieldScreen(context, viewId, fieldInfo), - onValueChanged: (value) { - final newVisibility = fieldInfo.visibility!.toggle(); - context.read().add( - DatabasePropertyEvent.setFieldVisibility( - fieldInfo.id, - newVisibility, - ), - ); - }, - ); - } - } -} - -class _NewDatabaseFieldTile extends StatelessWidget { - const _NewDatabaseFieldTile({required this.viewId}); - - final String viewId; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.text( - text: LocaleKeys.grid_field_newProperty.tr(), - leftIcon: FlowySvg( - FlowySvgs.add_s, - size: const Size.square(20), - color: Theme.of(context).hintColor, - ), - textColor: Theme.of(context).hintColor, - onTap: () => mobileCreateFieldWorkflow(context, viewId), - ); - } -} 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 deleted file mode 100644 index 009468a8f1..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart +++ /dev/null @@ -1,581 +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/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_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 'database_sort_bottom_sheet_cubit.dart'; - -class MobileSortEditor extends StatefulWidget { - const MobileSortEditor({ - super.key, - }); - - @override - State createState() => _MobileSortEditorState(); -} - -class _MobileSortEditorState extends State { - final PageController _pageController = PageController(); - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => MobileSortEditorCubit( - pageController: _pageController, - ), - child: Column( - children: [ - const _Header(), - SizedBox( - height: 400, //314, - child: PageView.builder( - controller: _pageController, - itemCount: 2, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return index == 0 - ? Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - child: const _Overview(), - ) - : const _SortDetail(); - }, - ), - ), - ], - ), - ); - } -} - -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 (state.showBackButton) - 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_sort.tr(), - fontSize: 16.0, - ), - ), - if (state.isCreatingNewSort) - Align( - alignment: Alignment.centerRight, - child: AppBarSaveButton( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - enable: state.newSortFieldId != null, - onTap: () { - _tryCreateSort(context, state); - context.read().returnToOverview(); - }, - ), - ), - ], - ), - ); - }, - ); - } - - void _tryCreateSort(BuildContext context, MobileSortEditorState state) { - if (state.newSortFieldId != null && state.newSortCondition != null) { - context.read().add( - SortEditorEvent.createSort( - fieldId: state.newSortFieldId!, - condition: state.newSortCondition!, - ), - ); - } - } -} - -class _Overview extends StatelessWidget { - const _Overview(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - Expanded( - child: state.sorts.isEmpty - ? Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.sort_descending_s, - size: const Size.square(60), - color: Theme.of(context).hintColor, - ), - FlowyText( - LocaleKeys.grid_sort_empty.tr(), - color: Theme.of(context).hintColor, - ), - ], - ), - ) - : ReorderableListView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - proxyDecorator: (child, index, animation) => Material( - color: Colors.transparent, - child: child, - ), - onReorder: (oldIndex, newIndex) => context - .read() - .add(SortEditorEvent.reorderSort(oldIndex, newIndex)), - itemCount: state.sorts.length, - itemBuilder: (context, index) => _SortItem( - key: ValueKey("sort_item_$index"), - sort: state.sorts[index], - ), - ), - ), - 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: () { - final firstField = context - .read() - .state - .creatableFields - .firstOrNull; - if (firstField == null) { - Fluttertoast.showToast( - msg: LocaleKeys.grid_sort_cannotFindCreatableField.tr(), - gravity: ToastGravity.BOTTOM, - ); - } else { - context.read().startCreatingSort(); - } - }, - 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_sort_addSort.tr(), - fontSize: 15, - ), - ], - ), - ), - ), - ), - ], - ); - }, - ); - } -} - -class _SortItem extends StatelessWidget { - const _SortItem({super.key, required this.sort}); - - final DatabaseSort sort; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric( - vertical: 4.0, - ), - decoration: BoxDecoration( - color: Theme.of(context).hoverColor, - borderRadius: BorderRadius.circular(12), - ), - child: Stack( - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => context - .read() - .startEditingSort(sort.sortId), - child: 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_sort_by.tr(), - fontSize: 15, - ), - ), - const VSpace(10), - Row( - children: [ - Expanded( - 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: 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, - ); - }, - ), - ), - const HSpace(6.0), - FlowySvg( - FlowySvgs.icon_right_small_ccm_outlined_s, - size: const Size.square(14), - color: Theme.of(context).hintColor, - ), - ], - ), - ), - ), - ), - const HSpace(6), - Expanded( - 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 EdgeInsetsDirectional.only( - start: 12, - end: 10, - ), - child: Center( - child: Row( - children: [ - Expanded( - child: FlowyText( - sort.condition.name, - ), - ), - const HSpace(6.0), - FlowySvg( - FlowySvgs.icon_right_small_ccm_outlined_s, - size: const Size.square(14), - color: Theme.of(context).hintColor, - ), - ], - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - Positioned( - right: 8, - top: 6, - child: InkWell( - onTap: () => context - .read() - .add(SortEditorEvent.deleteSort(sort.sortId)), - // 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 _SortDetail extends StatelessWidget { - const _SortDetail(); - - @override - Widget build(BuildContext context) { - final isCreatingNewSort = - context.read().state.isCreatingNewSort; - - return isCreatingNewSort - ? const _SortDetailContent() - : BlocSelector( - selector: (state) => state.sorts.firstWhere( - (sort) => - sort.sortId == - context.read().state.editingSortId, - ), - builder: (context, sort) { - return _SortDetailContent(sort: sort); - }, - ); - } -} - -class _SortDetailContent extends StatelessWidget { - const _SortDetailContent({ - this.sort, - }); - - final DatabaseSort? sort; - - bool get isCreatingNewSort => sort == null; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: DefaultTabController( - length: 2, - initialIndex: isCreatingNewSort - ? 0 - : sort!.condition == SortConditionPB.Ascending - ? 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) { - final newCondition = index == 0 - ? SortConditionPB.Ascending - : SortConditionPB.Descending; - _changeCondition(context, newCondition); - }, - tabs: [ - Tab( - height: 34, - child: Center( - child: FlowyText( - LocaleKeys.grid_sort_ascending.tr(), - fontSize: 14, - ), - ), - ), - Tab( - height: 34, - child: Center( - child: FlowyText( - LocaleKeys.grid_sort_descending.tr(), - fontSize: 14, - ), - ), - ), - ], - ), - ), - ), - ), - const VSpace(20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: FlowyText( - LocaleKeys.grid_settings_sortBy.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, state) { - final fields = state.allFields - .where((field) => field.fieldType.canCreateSort) - .toList(); - return ListView.builder( - itemCount: fields.length, - itemBuilder: (context, index) { - final fieldInfo = fields[index]; - final isSelected = isCreatingNewSort - ? context - .watch() - .state - .newSortFieldId == - fieldInfo.id - : sort!.fieldId == fieldInfo.id; - - final canSort = - fieldInfo.fieldType.canCreateSort && !fieldInfo.hasSort; - final beingEdited = - !isCreatingNewSort && sort!.fieldId == fieldInfo.id; - final enabled = canSort || beingEdited; - - return FlowyOptionTile.checkbox( - text: fieldInfo.field.name, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - ), - isSelected: isSelected, - textColor: enabled ? null : Theme.of(context).disabledColor, - showTopBorder: false, - onTap: () { - if (isSelected) { - return; - } - if (enabled) { - _changeFieldId(context, fieldInfo.id); - } else { - Fluttertoast.showToast( - msg: LocaleKeys.grid_sort_fieldInUse.tr(), - gravity: ToastGravity.BOTTOM, - ); - } - }, - ); - }, - ); - }, - ), - ), - ], - ); - } - - void _changeCondition(BuildContext context, SortConditionPB newCondition) { - if (isCreatingNewSort) { - context.read().changeSortCondition(newCondition); - } else { - context.read().add( - SortEditorEvent.editSort( - sortId: sort!.sortId, - condition: newCondition, - ), - ); - } - } - - void _changeFieldId(BuildContext context, String newFieldId) { - if (isCreatingNewSort) { - context.read().changeFieldId(newFieldId); - } else { - context.read().add( - SortEditorEvent.editSort( - sortId: sort!.sortId, - fieldId: newFieldId, - ), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet_cubit.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet_cubit.dart deleted file mode 100644 index 684f98e564..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet_cubit.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'database_sort_bottom_sheet_cubit.freezed.dart'; - -class MobileSortEditorCubit extends Cubit { - MobileSortEditorCubit({ - required this.pageController, - }) : super(MobileSortEditorState.initial()); - - final PageController pageController; - - void returnToOverview() { - _animateToPage(0); - emit(MobileSortEditorState.initial()); - } - - void startCreatingSort() { - _animateToPage(1); - emit( - state.copyWith( - showBackButton: true, - isCreatingNewSort: true, - newSortCondition: SortConditionPB.Ascending, - ), - ); - } - - void startEditingSort(String sortId) { - _animateToPage(1); - emit( - state.copyWith( - showBackButton: true, - editingSortId: sortId, - ), - ); - } - - /// only used when creating a new sort - void changeFieldId(String fieldId) { - emit(state.copyWith(newSortFieldId: fieldId)); - } - - /// only used when creating a new sort - void changeSortCondition(SortConditionPB condition) { - emit(state.copyWith(newSortCondition: condition)); - } - - Future _animateToPage(int page) async { - return pageController.animateToPage( - page, - duration: const Duration(milliseconds: 150), - curve: Curves.easeOut, - ); - } -} - -@freezed -class MobileSortEditorState with _$MobileSortEditorState { - factory MobileSortEditorState({ - required bool showBackButton, - required String? editingSortId, - required bool isCreatingNewSort, - required String? newSortFieldId, - required SortConditionPB? newSortCondition, - }) = _MobileSortEditorState; - - factory MobileSortEditorState.initial() => MobileSortEditorState( - showBackButton: false, - editingSortId: null, - isCreatingNewSort: false, - newSortFieldId: null, - newSortCondition: null, - ); -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart deleted file mode 100644 index fdce3d6d21..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart +++ /dev/null @@ -1,202 +0,0 @@ -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/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/database_layout_ext.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../field/mobile_field_bottom_sheets.dart'; - -/// [DatabaseViewLayoutPicker] is seen when changing the layout type of a -/// database view or creating a new database view. -class DatabaseViewLayoutPicker extends StatelessWidget { - const DatabaseViewLayoutPicker({ - super.key, - required this.selectedLayout, - required this.onSelect, - }); - - final DatabaseLayoutPB selectedLayout; - final void Function(DatabaseLayoutPB layout) onSelect; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildButton(DatabaseLayoutPB.Grid, true), - _buildButton(DatabaseLayoutPB.Board, false), - _buildButton(DatabaseLayoutPB.Calendar, false), - ], - ); - } - - Widget _buildButton(DatabaseLayoutPB layout, bool showTopBorder) { - return FlowyOptionTile.checkbox( - text: layout.layoutName, - leftIcon: FlowySvg(layout.icon, size: const Size.square(20)), - isSelected: selectedLayout == layout, - showTopBorder: showTopBorder, - onTap: () { - onSelect(layout); - }, - ); - } -} - -/// [MobileCalendarViewLayoutSettings] is used when the database layout is -/// calendar. It allows changing the field being used to layout the events, -/// and which day of the week the calendar starts on. -class MobileCalendarViewLayoutSettings extends StatelessWidget { - const MobileCalendarViewLayoutSettings({ - super.key, - required this.databaseController, - }); - - final DatabaseController databaseController; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return CalendarSettingBloc( - databaseController: databaseController, - )..add(const CalendarSettingEvent.initial()); - }, - child: BlocBuilder( - builder: (context, state) { - if (state.layoutSetting == null) { - return const SizedBox.shrink(); - } - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _CalendarLayoutField( - context: context, - databaseController: databaseController, - selectedFieldId: state.layoutSetting?.fieldId, - ), - _divider(), - ..._startWeek(context, state.layoutSetting?.firstDayOfWeek), - ], - ); - }, - ), - ); - } - - List _startWeek(BuildContext context, int? firstDayOfWeek) { - final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; - return [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0), - child: FlowyText( - LocaleKeys.calendar_settings_firstDayOfWeek.tr().toUpperCase(), - fontSize: 13, - color: Theme.of(context).hintColor, - ), - ), - FlowyOptionTile.checkbox( - text: symbols.WEEKDAYS[0], - isSelected: firstDayOfWeek! == 0, - onTap: () { - context.read().add( - const CalendarSettingEvent.updateLayoutSetting( - firstDayOfWeek: 0, - ), - ); - }, - ), - FlowyOptionTile.checkbox( - text: symbols.WEEKDAYS[1], - isSelected: firstDayOfWeek == 1, - showTopBorder: false, - onTap: () { - context.read().add( - const CalendarSettingEvent.updateLayoutSetting( - firstDayOfWeek: 1, - ), - ); - }, - ), - ]; - } - - Widget _divider() => const VSpace(20); -} - -class _CalendarLayoutField extends StatelessWidget { - const _CalendarLayoutField({ - required this.context, - required this.databaseController, - required this.selectedFieldId, - }); - - final BuildContext context; - final DatabaseController databaseController; - final String? selectedFieldId; - - @override - Widget build(BuildContext context) { - FieldInfo? selectedField; - if (selectedFieldId != null) { - selectedField = - databaseController.fieldController.getField(selectedFieldId!); - } - return FlowyOptionTile.text( - text: LocaleKeys.calendar_settings_layoutDateField.tr(), - trailing: selectedFieldId == null - ? null - : Row( - children: [ - FlowyText( - selectedField!.name, - color: Theme.of(context).hintColor, - ), - const HSpace(8), - const FlowySvg(FlowySvgs.arrow_right_s), - ], - ), - onTap: () async { - final newFieldId = await showFieldPicker( - context, - LocaleKeys.calendar_settings_changeLayoutDateField.tr(), - selectedFieldId, - databaseController.fieldController, - (field) => field.fieldType == FieldType.DateTime, - ); - if (context.mounted && - newFieldId != null && - newFieldId != selectedFieldId) { - context.read().add( - CalendarSettingEvent.updateLayoutSetting( - layoutFieldId: newFieldId, - ), - ); - } - }, - ); - } -} - -class MobileBoardViewLayoutSettings extends StatelessWidget { - const MobileBoardViewLayoutSettings({ - super.key, - required this.databaseController, - }); - - final DatabaseController databaseController; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.text(text: LocaleKeys.board_groupBy.tr()); - } -} 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 deleted file mode 100644 index f7ad313412..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart +++ /dev/null @@ -1,315 +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/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'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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 'database_view_layout.dart'; -import 'database_view_quick_actions.dart'; - -/// [MobileDatabaseViewList] shows a list of all the views in the database and -/// adds a button to create a new database view. -class MobileDatabaseViewList extends StatelessWidget { - const MobileDatabaseViewList({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final views = [state.view, ...state.view.childViews]; - - return Column( - children: [ - _Header( - title: LocaleKeys.grid_settings_viewList.plural( - context.watch().state.tabBars.length, - namedArgs: { - 'count': - '${context.watch().state.tabBars.length}', - }, - ), - showBackButton: false, - useFilledDoneButton: false, - onDone: (context) => Navigator.pop(context), - ), - Expanded( - child: ListView( - shrinkWrap: true, - padding: EdgeInsets.zero, - children: [ - ...views.mapIndexed( - (index, view) => MobileDatabaseViewListButton( - view: view, - showTopBorder: index == 0, - ), - ), - const VSpace(20), - const MobileNewDatabaseViewButton(), - VSpace( - context.bottomSheetPadding(ignoreViewPadding: false), - ), - ], - ), - ), - ], - ); - }, - ); - } -} - -/// Same header as the one in showMobileBottomSheet, but allows popping the -/// sheet with a value. -class _Header extends StatelessWidget { - const _Header({ - required this.title, - required this.showBackButton, - required this.useFilledDoneButton, - required this.onDone, - }); - - final String title; - final bool showBackButton; - final bool useFilledDoneButton; - final void Function(BuildContext context) onDone; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: SizedBox( - height: 44.0, - child: Stack( - children: [ - if (showBackButton) - const Align( - alignment: Alignment.centerLeft, - child: AppBarBackButton(), - ), - Align( - child: FlowyText.medium( - title, - fontSize: 16.0, - ), - ), - useFilledDoneButton - ? Align( - alignment: Alignment.centerRight, - child: AppBarFilledDoneButton( - onTap: () => onDone(context), - ), - ) - : Align( - alignment: Alignment.centerRight, - child: AppBarDoneButton( - onTap: () => onDone(context), - ), - ), - ], - ), - ), - ); - } -} - -@visibleForTesting -class MobileDatabaseViewListButton extends StatelessWidget { - const MobileDatabaseViewListButton({ - super.key, - required this.view, - required this.showTopBorder, - }); - - final ViewPB view; - final bool showTopBorder; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final index = - state.tabBars.indexWhere((tabBar) => tabBar.viewId == view.id); - final isSelected = index == state.selectedIndex; - return FlowyOptionTile.text( - text: view.name, - onTap: () { - context - .read() - .add(DatabaseTabBarEvent.selectView(view.id)); - }, - leftIcon: _buildViewIconButton(context, view), - trailing: _trailing( - context, - state.tabBarControllerByViewId[view.id]!.controller, - isSelected, - ), - showTopBorder: showTopBorder, - ); - }, - ); - } - - 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, - ); - } - - Widget _trailing( - BuildContext context, - DatabaseController databaseController, - bool isSelected, - ) { - final more = FlowyIconButton( - icon: FlowySvg( - FlowySvgs.three_dots_s, - size: const Size.square(20), - color: Theme.of(context).hintColor, - ), - onPressed: () { - showMobileBottomSheet( - context, - showDragHandle: true, - backgroundColor: AFThemeExtension.of(context).background, - builder: (_) { - return BlocProvider( - create: (_) => - ViewBloc(view: view)..add(const ViewEvent.initial()), - child: MobileDatabaseViewQuickActions( - view: view, - databaseController: databaseController, - ), - ); - }, - ); - }, - ); - if (isSelected) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FlowySvg( - FlowySvgs.m_blue_check_s, - size: Size.square(20), - blendMode: BlendMode.dst, - ), - const HSpace(8), - more, - ], - ); - } else { - return more; - } - } -} - -class MobileNewDatabaseViewButton extends StatelessWidget { - const MobileNewDatabaseViewButton({super.key}); - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.text( - text: LocaleKeys.grid_settings_createView.tr(), - textColor: Theme.of(context).hintColor, - leftIcon: FlowySvg( - FlowySvgs.add_s, - size: const Size.square(20), - color: Theme.of(context).hintColor, - ), - onTap: () async { - final result = await showMobileBottomSheet<(DatabaseLayoutPB, String)>( - context, - showDragHandle: true, - builder: (_) { - return const MobileCreateDatabaseView(); - }, - ); - if (context.mounted && result != null) { - context - .read() - .add(DatabaseTabBarEvent.createView(result.$1, result.$2)); - } - }, - ); - } -} - -class MobileCreateDatabaseView extends StatefulWidget { - const MobileCreateDatabaseView({super.key}); - - @override - State createState() => - _MobileCreateDatabaseViewState(); -} - -class _MobileCreateDatabaseViewState extends State { - late final TextEditingController controller; - DatabaseLayoutPB layoutType = DatabaseLayoutPB.Grid; - - @override - void initState() { - super.initState(); - controller = TextEditingController( - text: LocaleKeys.grid_title_placeholder.tr(), - ); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - _Header( - title: LocaleKeys.grid_settings_createView.tr(), - showBackButton: true, - useFilledDoneButton: true, - onDone: (context) => - context.pop((layoutType, controller.text.trim())), - ), - FlowyOptionTile.textField( - autofocus: true, - controller: controller, - ), - const VSpace(20), - DatabaseViewLayoutPicker( - selectedLayout: layoutType, - onSelect: (layout) { - setState(() => layoutType = layout); - }, - ), - ], - ); - } -} 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 deleted file mode 100644 index a133739a9d..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart +++ /dev/null @@ -1,166 +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/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'; - -import 'edit_database_view_screen.dart'; - -/// [MobileDatabaseViewQuickActions] is gives users to quickly edit a database -/// view from the [MobileDatabaseViewList] -class MobileDatabaseViewQuickActions extends StatelessWidget { - const MobileDatabaseViewQuickActions({ - super.key, - required this.view, - required this.databaseController, - }); - - final ViewPB view; - final DatabaseController databaseController; - - @override - Widget build(BuildContext context) { - final isInline = view.childViews.isNotEmpty; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _actionButton(context, _Action.edit, () async { - final bloc = context.read(); - await showTransitionMobileBottomSheet( - context, - showHeader: true, - showDoneButton: true, - title: LocaleKeys.grid_settings_editView.tr(), - builder: (_) => BlocProvider.value( - value: bloc, - child: MobileEditDatabaseViewScreen( - databaseController: databaseController, - ), - ), - ); - if (context.mounted) { - 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(), - _actionButton( - context, - _Action.duplicate, - () { - context.read().add(const ViewEvent.duplicate()); - context.pop(); - }, - !isInline, - ), - const MobileQuickActionDivider(), - _actionButton( - context, - _Action.delete, - () { - context.read().add(const ViewEvent.delete()); - context.pop(); - }, - !isInline, - ), - ], - ); - } - - Widget _actionButton( - BuildContext context, - _Action action, - VoidCallback onTap, [ - bool enable = true, - ]) { - return MobileQuickActionButton( - icon: action.icon, - text: action.label, - textColor: action.color(context), - iconColor: action.color(context), - onTap: onTap, - enable: enable, - ); - } -} - -enum _Action { - edit, - changeIcon, - delete, - duplicate; - - 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(), - }; - } - - FlowySvgData get icon { - return switch (this) { - edit => FlowySvgs.view_item_rename_s, - duplicate => FlowySvgs.duplicate_s, - delete => FlowySvgs.trash_s, - changeIcon => FlowySvgs.change_icon_s, - }; - } - - Color? color(BuildContext context) { - return switch (this) { - delete => Theme.of(context).colorScheme.error, - _ => null, - }; - } -} 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 deleted file mode 100644 index f5812541c8..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart +++ /dev/null @@ -1,288 +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/flowy_option_tile.dart'; -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/domain/database_view_service.dart'; -import 'package:appflowy/plugins/database/domain/layout_service.dart'; -import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package: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 'database_field_list.dart'; -import 'database_view_layout.dart'; - -/// [MobileEditDatabaseViewScreen] is the main widget used to edit a database -/// view. It contains multiple sub-pages, and the current page is managed by -/// [MobileEditDatabaseViewCubit] -class MobileEditDatabaseViewScreen extends StatelessWidget { - const MobileEditDatabaseViewScreen({ - super.key, - required this.databaseController, - }); - - final DatabaseController databaseController; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - _NameAndIcon(view: state.view), - _divider(), - DatabaseViewSettingTile( - setting: DatabaseViewSettings.layout, - databaseController: databaseController, - view: state.view, - showTopBorder: true, - ), - if (databaseController.databaseLayout == DatabaseLayoutPB.Calendar) - DatabaseViewSettingTile( - setting: DatabaseViewSettings.calendar, - databaseController: databaseController, - view: state.view, - ), - DatabaseViewSettingTile( - setting: DatabaseViewSettings.fields, - databaseController: databaseController, - view: state.view, - ), - _divider(), - ], - ); - }, - ); - } - - Widget _divider() => const VSpace(20); -} - -class _NameAndIcon extends StatefulWidget { - const _NameAndIcon({required this.view}); - - final ViewPB view; - - @override - State<_NameAndIcon> createState() => _NameAndIconState(); -} - -class _NameAndIconState extends State<_NameAndIcon> { - final TextEditingController textEditingController = TextEditingController(); - - @override - void initState() { - super.initState(); - textEditingController.text = widget.view.name; - } - - @override - void dispose() { - textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Material( - child: FlowyOptionTile.textField( - autofocus: true, - showTopBorder: false, - controller: textEditingController, - onTextChanged: (text) { - context.read().add(ViewEvent.rename(text)); - }, - ), - ); - } -} - -enum DatabaseViewSettings { - layout, - fields, - filter, - sort, - board, - calendar, - duplicate, - delete; - - String get label { - return switch (this) { - layout => LocaleKeys.grid_settings_databaseLayout.tr(), - fields => LocaleKeys.grid_settings_properties.tr(), - filter => LocaleKeys.grid_settings_filter.tr(), - sort => LocaleKeys.grid_settings_sort.tr(), - board => LocaleKeys.grid_settings_boardSettings.tr(), - calendar => LocaleKeys.grid_settings_calendarSettings.tr(), - duplicate => LocaleKeys.grid_settings_duplicateView.tr(), - delete => LocaleKeys.grid_settings_deleteView.tr(), - }; - } - - FlowySvgData get icon { - return switch (this) { - layout => FlowySvgs.card_view_s, - fields => FlowySvgs.disorder_list_s, - filter => FlowySvgs.filter_s, - sort => FlowySvgs.sort_ascending_s, - board => FlowySvgs.board_s, - calendar => FlowySvgs.calendar_s, - duplicate => FlowySvgs.copy_s, - delete => FlowySvgs.delete_s, - }; - } -} - -class DatabaseViewSettingTile extends StatelessWidget { - const DatabaseViewSettingTile({ - super.key, - required this.setting, - required this.databaseController, - required this.view, - this.showTopBorder = false, - }); - - final DatabaseViewSettings setting; - final DatabaseController databaseController; - final ViewPB view; - final bool showTopBorder; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.text( - text: setting.label, - leftIcon: FlowySvg(setting.icon, size: const Size.square(20)), - trailing: _trailing(context, setting, view, databaseController), - showTopBorder: showTopBorder, - onTap: () => _onTap(context), - ); - } - - Widget _trailing( - BuildContext context, - DatabaseViewSettings setting, - ViewPB view, - DatabaseController databaseController, - ) { - switch (setting) { - case DatabaseViewSettings.layout: - return Row( - children: [ - FlowyText( - lineHeight: 1.0, - databaseLayoutFromViewLayout(view.layout).layoutName, - color: Theme.of(context).hintColor, - ), - const HSpace(8), - const FlowySvg(FlowySvgs.arrow_right_s), - ], - ); - case DatabaseViewSettings.fields: - final numVisible = databaseController.fieldController.fieldInfos - .where((field) => field.visibility != FieldVisibility.AlwaysHidden) - .length; - return Row( - children: [ - FlowyText( - LocaleKeys.grid_settings_numberOfVisibleFields - .tr(args: [numVisible.toString()]), - color: Theme.of(context).hintColor, - ), - const HSpace(8), - const FlowySvg(FlowySvgs.arrow_right_s), - ], - ); - default: - return const SizedBox.shrink(); - } - } - - void _onTap(BuildContext context) async { - if (setting == DatabaseViewSettings.layout) { - final databaseLayout = databaseLayoutFromViewLayout(view.layout); - final newLayout = await showMobileBottomSheet( - context, - showDragHandle: true, - showHeader: true, - showDivider: false, - title: LocaleKeys.grid_settings_layout.tr(), - builder: (context) { - return DatabaseViewLayoutPicker( - selectedLayout: databaseLayout, - onSelect: (layout) => Navigator.of(context).pop(layout), - ); - }, - ); - if (newLayout != null && newLayout != databaseLayout) { - await DatabaseViewBackendService.updateLayout( - viewId: databaseController.viewId, - layout: newLayout, - ); - } - return; - } - - if (setting == DatabaseViewSettings.fields) { - await showTransitionMobileBottomSheet( - context, - showHeader: true, - showBackButton: true, - title: LocaleKeys.grid_settings_properties.tr(), - builder: (_) { - return BlocProvider.value( - value: context.read(), - child: MobileDatabaseFieldList( - databaseController: databaseController, - canCreate: true, - ), - ); - }, - ); - return; - } - - if (setting == DatabaseViewSettings.board) { - await showMobileBottomSheet( - context, - builder: (context) { - return Padding( - padding: const EdgeInsets.only(top: 24, bottom: 46), - child: MobileBoardViewLayoutSettings( - databaseController: databaseController, - ), - ); - }, - ); - return; - } - - if (setting == DatabaseViewSettings.calendar) { - await showMobileBottomSheet( - context, - showDragHandle: true, - showHeader: true, - showDivider: false, - title: LocaleKeys.calendar_settings_name.tr(), - builder: (context) { - return MobileCalendarViewLayoutSettings( - databaseController: databaseController, - ); - }, - ); - return; - } - - if (setting == DatabaseViewSettings.delete) { - context.read().add(const ViewEvent.delete()); - context.pop(true); - return; - } - } -} 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 deleted file mode 100644 index 373558a480..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart +++ /dev/null @@ -1,45 +0,0 @@ -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'; - -class MobileDocumentScreen extends StatelessWidget { - const MobileDocumentScreen({ - 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) { - return MobileViewPage( - 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_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart deleted file mode 100644 index ca012891f6..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.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/user/user_workspace_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/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; - -class MobileFavoritePageFolder extends StatelessWidget { - const MobileFavoritePageFolder({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final workspaceId = - context.read().state.currentWorkspace?.workspaceId ?? - ''; - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => SidebarSectionsBloc() - ..add(SidebarSectionsEvent.initial(userProfile, workspaceId)), - ), - BlocProvider( - create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), - ), - ], - child: BlocListener( - listener: (context, state) => - context.read().add(const FavoriteEvent.initial()), - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) => - context.pushView(state.lastCreatedRootView!), - ), - ], - child: Builder( - builder: (context) { - final favoriteState = context.watch().state; - if (favoriteState.views.isEmpty) { - return FlowyMobileStateContainer.info( - emoji: '😁', - title: LocaleKeys.favorite_noFavorite.tr(), - description: LocaleKeys.favorite_noFavoriteHintText.tr(), - ); - } - return Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SlidableAutoCloseBehavior( - child: Column( - children: [ - MobileFavoriteFolder( - showHeader: false, - forceExpanded: true, - views: - favoriteState.views.map((e) => e.item).toList(), - ), - const VSpace(100.0), - ], - ), - ), - ), - ), - ); - }, - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index 0e7a7cb4c6..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dart'; -import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/workspace/application/user/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileFavoriteScreen extends StatelessWidget { - const MobileFavoriteScreen({ - super.key, - }); - - static const routeName = '/favorite'; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: Future.wait([ - FolderEventGetCurrentWorkspaceSetting().send(), - getIt().getUser(), - ]), - builder: (context, snapshots) { - if (!snapshots.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - final latest = snapshots.data?[0].fold( - (latest) { - return latest as WorkspaceLatestPB?; - }, - (error) => null, - ); - final userProfile = snapshots.data?[1].fold( - (userProfilePB) { - return userProfilePB as UserProfilePB?; - }, - (error) => null, - ); - - // 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) { - return const WorkspaceFailedScreen(); - } - - return Scaffold( - body: SafeArea( - child: BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: userProfile) - ..add( - const UserWorkspaceEvent.initial(), - ), - child: BlocBuilder( - buildWhen: (previous, current) => - previous.currentWorkspace?.workspaceId != - current.currentWorkspace?.workspaceId, - builder: (context, state) { - return MobileFavoritePage( - userProfile: userProfile, - ); - }, - ), - ), - ), - ); - }, - ); - } -} - -class MobileFavoritePage extends StatelessWidget { - const MobileFavoritePage({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // Header - Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: Platform.isAndroid ? 8.0 : 0.0, - ), - child: MobileHomePageHeader( - userProfile: userProfile, - ), - ), - const Divider(), - - // Folder - Expanded( - child: MobileFavoritePageFolder( - userProfile: userProfile, - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index 6282421109..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart'; -import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart'; -import 'package:appflowy/util/theme_extension.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/user/prelude.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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileFavoriteSpace extends StatefulWidget { - const MobileFavoriteSpace({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - State createState() => _MobileFavoriteSpaceState(); -} - -class _MobileFavoriteSpaceState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - final workspaceId = - context.read().state.currentWorkspace?.workspaceId ?? - ''; - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => SidebarSectionsBloc() - ..add( - SidebarSectionsEvent.initial(widget.userProfile, workspaceId), - ), - ), - BlocProvider( - create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), - ), - ], - child: BlocListener( - listener: (context, state) => - context.read().add(const FavoriteEvent.initial()), - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) => - context.pushView(state.lastCreatedRootView!), - ), - ], - child: Builder( - builder: (context) { - final favoriteState = context.watch().state; - - if (favoriteState.isLoading) { - return const SizedBox.shrink(); - } - - if (favoriteState.views.isEmpty) { - return const EmptySpacePlaceholder( - type: MobilePageCardType.favorite, - ); - } - - return _FavoriteViews( - favoriteViews: favoriteState.views.reversed.toList(), - ); - }, - ), - ), - ), - ); - } -} - -class _FavoriteViews extends StatelessWidget { - const _FavoriteViews({ - required this.favoriteViews, - }); - - final List favoriteViews; - - @override - Widget build(BuildContext context) { - 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, - ), - ), - ), - 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 deleted file mode 100644 index 1efee460eb..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; -import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart'; -import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileFavoriteFolder extends StatelessWidget { - const MobileFavoriteFolder({ - super.key, - required this.views, - this.showHeader = true, - this.forceExpanded = false, - }); - - final bool showHeader; - final bool forceExpanded; - final List views; - - @override - Widget build(BuildContext context) { - if (views.isEmpty) { - return const SizedBox.shrink(); - } - - return BlocProvider( - create: (context) => FolderBloc(type: FolderSpaceType.favorite) - ..add( - const FolderEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - if (showHeader) ...[ - MobileFavoriteFolderHeader( - isExpanded: context.read().state.isExpanded, - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context.read().add( - const FolderEvent.expandOrUnExpand(isExpanded: true), - ), - ), - const VSpace(8.0), - const Divider( - height: 1, - ), - ], - if (forceExpanded || state.isExpanded) - ...views.map( - (view) => MobileViewItem( - key: ValueKey( - '${FolderSpaceType.favorite.name} ${view.id}', - ), - spaceType: FolderSpaceType.favorite, - isDraggable: false, - isFirstChild: view.id == views.first.id, - isFeedback: false, - view: view, - level: 0, - onSelected: context.pushView, - endActionPane: (context) => buildEndActionPane( - context, - [ - view.isFavorite - ? MobilePaneActionType.removeFromFavorites - : MobilePaneActionType.addToFavorites, - MobilePaneActionType.more, - ], - spaceType: FolderSpaceType.favorite, - spaceRatio: 5, - ), - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart deleted file mode 100644 index b5ae0ad1bd..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart +++ /dev/null @@ -1,59 +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'; - -class MobileFavoriteFolderHeader extends StatefulWidget { - const MobileFavoriteFolderHeader({ - super.key, - required this.onPressed, - required this.onAdded, - required this.isExpanded, - }); - - final VoidCallback onPressed; - final VoidCallback onAdded; - final bool isExpanded; - - @override - State createState() => - _MobileFavoriteFolderHeaderState(); -} - -class _MobileFavoriteFolderHeaderState - extends State { - double _turns = 0; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: FlowyButton( - text: FlowyText.semibold( - LocaleKeys.sideBar_favorites.tr(), - fontSize: 20.0, - ), - margin: const EdgeInsets.symmetric(vertical: 8), - expandText: false, - mainAxisAlignment: MainAxisAlignment.start, - rightIcon: AnimatedRotation( - duration: const Duration(milliseconds: 200), - turns: _turns, - child: const Icon( - Icons.keyboard_arrow_down_rounded, - color: Colors.grey, - ), - ), - onTap: () { - setState(() { - _turns = widget.isExpanded ? -0.25 : 0; - }); - widget.onPressed(); - }, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/home.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/home.dart deleted file mode 100644 index 31722276f9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/home.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'mobile_home_page.dart'; -export 'mobile_home_setting_page.dart'; -export 'mobile_home_trash_page.dart'; 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 deleted file mode 100644 index 5651379522..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:appflowy/mobile/presentation/home/mobile_folders.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileHomeSpace extends StatefulWidget { - const MobileHomeSpace({super.key, required this.userProfile}); - - final UserProfilePB userProfile; - - @override - State createState() => _MobileHomeSpaceState(); -} - -class _MobileHomeSpaceState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - 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, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart deleted file mode 100644 index 0013650df9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.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/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/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; - -// Contains Public And Private Sections -class MobileFolders extends StatelessWidget { - const MobileFolders({ - super.key, - required this.user, - required this.workspaceId, - required this.showFavorite, - }); - - final UserProfilePB user; - final String workspaceId; - final bool showFavorite; - - @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), - ], - ), - ); - }, - ); - } - - List _buildSpaceOrSection( - BuildContext context, - SidebarSectionsState state, - ) { - if (context.watch().state.spaces.isNotEmpty) { - return [ - const MobileSpace(), - ]; - } - - if (context.read().state.isCollabWorkspaceOn) { - return [ - MobileSectionFolder( - title: LocaleKeys.sideBar_workspace.tr(), - spaceType: FolderSpaceType.public, - views: state.section.publicViews, - ), - const VSpace(8.0), - MobileSectionFolder( - title: LocaleKeys.sideBar_private.tr(), - spaceType: FolderSpaceType.private, - views: state.section.privateViews, - ), - ]; - } - - return [ - MobileSectionFolder( - title: LocaleKeys.sideBar_personal.tr(), - spaceType: FolderSpaceType.public, - views: state.section.publicViews, - ), - ]; - } -} 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 deleted file mode 100644 index fdea8322c3..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ /dev/null @@ -1,335 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; -import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; -import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/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}); - - static const routeName = '/home'; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: Future.wait([ - FolderEventGetCurrentWorkspaceSetting().send(), - getIt().getUser(), - ]), - builder: (context, snapshots) { - if (!snapshots.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - final workspaceLatest = snapshots.data?[0].fold( - (workspaceLatestPB) { - return workspaceLatestPB as WorkspaceLatestPB?; - }, - (error) => null, - ); - final userProfile = snapshots.data?[1].fold( - (userProfilePB) { - return userProfilePB as UserProfilePB?; - }, - (error) => null, - ); - - // 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) { - return const WorkspaceFailedScreen(); - } - - Sentry.configureScope( - (scope) => scope.setUser( - SentryUser( - id: userProfile.id.toString(), - ), - ), - ); - - return Scaffold( - body: SafeArea( - bottom: false, - child: Provider.value( - value: userProfile, - child: MobileHomePage( - userProfile: userProfile, - workspaceLatest: workspaceLatest, - ), - ), - ), - ); - }, - ); - } -} - -final PropertyValueNotifier mCurrentWorkspace = - PropertyValueNotifier(null); - -class MobileHomePage extends StatefulWidget { - const MobileHomePage({ - super.key, - required this.userProfile, - required this.workspaceLatest, - }); - - final UserProfilePB userProfile; - final WorkspaceLatestPB workspaceLatest; - - @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(); - } - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: widget.userProfile) - ..add(const UserWorkspaceEvent.initial()), - ), - BlocProvider( - create: (context) => - FavoriteBloc()..add(const FavoriteEvent.initial()), - ), - BlocProvider.value( - value: getIt()..add(const ReminderEvent.started()), - ), - ], - child: _HomePage(userProfile: widget.userProfile), - ); - } - - void _onLatestViewChange() async { - final id = getIt().latestOpenView?.id; - if (id == null) { - return; - } - await FolderEventSetLatestView(ViewIdPB(value: id)).send(); - } -} - -class _HomePage extends StatefulWidget { - const _HomePage({required this.userProfile}); - - final UserProfilePB userProfile; - - @override - State<_HomePage> createState() => _HomePageState(); -} - -class _HomePageState extends State<_HomePage> { - Loading? loadingIndicator; - - @override - Widget build(BuildContext context) { - return BlocConsumer( - buildWhen: (previous, current) => - previous.currentWorkspace?.workspaceId != - current.currentWorkspace?.workspaceId, - listener: (context, state) { - getIt().reset(); - mCurrentWorkspace.value = state.currentWorkspace; - - Debounce.debounce( - 'workspace_action_result', - const Duration(milliseconds: 150), - () { - _showResultDialog(context, state); - }, - ); - }, - builder: (context, state) { - if (state.currentWorkspace == null) { - return const SizedBox.shrink(); - } - - final workspaceId = state.currentWorkspace!.workspaceId; - - return Column( - key: ValueKey('mobile_home_page_$workspaceId'), - children: [ - // Header - Padding( - padding: const EdgeInsets.only( - left: HomeSpaceViewSizes.mHorizontalPadding, - right: 8.0, - ), - child: MobileHomePageHeader( - userProfile: widget.userProfile, - ), - ), - - Expanded( - child: 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 deleted file mode 100644 index 113f12e543..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ /dev/null @@ -1,255 +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/home/workspaces/workspace_menu_bottom_sheet.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/built_in_svgs.dart'; -import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; -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_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -import 'setting/settings_popup_menu.dart'; - -class MobileHomePageHeader extends StatelessWidget { - const MobileHomePageHeader({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(param1: userProfile) - ..add(const SettingsUserEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final isCollaborativeWorkspace = - context.read().state.isCollabWorkspaceOn; - return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 56), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: isCollaborativeWorkspace - ? _MobileWorkspace(userProfile: userProfile) - : _MobileUser(userProfile: userProfile), - ), - HomePageSettingsPopupMenu( - userProfile: userProfile, - ), - const HSpace(8.0), - ], - ), - ); - }, - ), - ); - } -} - -class _MobileUser extends StatelessWidget { - const _MobileUser({ - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final userIcon = userProfile.iconUrl; - return Row( - children: [ - _UserIcon(userIcon: userIcon), - const HSpace(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const FlowyText.medium('AppFlowy', fontSize: 18), - const VSpace(4), - FlowyText.regular( - userProfile.email.isNotEmpty - ? userProfile.email - : userProfile.name, - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ); - } -} - -class _MobileWorkspace extends StatelessWidget { - const _MobileWorkspace({ - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final currentWorkspace = state.currentWorkspace; - if (currentWorkspace == null) { - return const SizedBox.shrink(); - } - return AnimatedGestureDetector( - scaleFactor: 0.99, - alignment: Alignment.centerLeft, - onTapUp: () { - context.read().add( - const UserWorkspaceEvent.fetchWorkspaces(), - ); - _showSwitchWorkspacesBottomSheet(context); - }, - 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, - ), - ), - ), - currentWorkspace.icon.isNotEmpty - ? const HSpace(2) - : const HSpace(8), - Flexible( - child: FlowyText.semibold( - currentWorkspace.name, - fontSize: 20.0, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }, - ); - } - - void _showSwitchWorkspacesBottomSheet( - BuildContext context, - ) { - showMobileBottomSheet( - context, - showDivider: false, - 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) { - return BlocProvider.value( - value: context.read(), - child: BlocBuilder( - builder: (context, state) { - final currentWorkspace = state.currentWorkspace; - final workspaces = state.workspaces; - if (currentWorkspace == null || workspaces.isEmpty) { - return const SizedBox.shrink(); - } - return MobileWorkspaceMenu( - userProfile: userProfile, - currentWorkspace: currentWorkspace, - workspaces: workspaces, - onWorkspaceSelected: (workspace) { - Navigator.of(sheetContext).pop(); - - if (workspace == currentWorkspace) { - return; - } - - context.read().add( - UserWorkspaceEvent.openWorkspace( - workspace.workspaceId, - workspace.workspaceAuthType, - ), - ); - }, - ); - }, - ), - ); - }, - ); - } -} - -class _UserIcon extends StatelessWidget { - const _UserIcon({ - required this.userIcon, - }); - - final String userIcon; - - @override - Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - text: builtInSVGIcons.contains(userIcon) - // to be compatible with old user icon - ? FlowySvg( - FlowySvgData('emoji/$userIcon'), - size: const Size.square(32), - blendMode: null, - ) - : FlowyText( - userIcon.isNotEmpty ? userIcon : '🐻', - fontSize: 26, - ), - onTap: () async { - final icon = await context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: { - MobileEmojiPickerScreen.pageTitle: - LocaleKeys.titleBar_userIcon.tr(), - MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name], - }, - ).toString(), - ); - if (icon != null) { - if (context.mounted) { - context.read().add( - SettingsUserEvent.updateUserIcon( - iconUrl: icon.emoji, - ), - ); - } - } - }, - ); - } -} 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 deleted file mode 100644 index a01df20549..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -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({ - super.key, - }); - - static const routeName = '/settings'; - - @override - State createState() => _MobileHomeSettingPageState(); -} - -class _MobileHomeSettingPageState extends State { - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: getIt().getUser(), - builder: (context, snapshot) { - String? errorMsg; - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - final userProfile = snapshot.data?.fold( - (userProfile) { - return userProfile; - }, - (error) { - errorMsg = error.msg; - return null; - }, - ); - - return Scaffold( - appBar: FlowyAppBar( - titleText: LocaleKeys.settings_title.tr(), - ), - body: userProfile == null - ? _buildErrorWidget(errorMsg) - : _buildSettingsWidget(userProfile), - ); - }, - ); - } - - Widget _buildErrorWidget(String? errorMsg) { - return FlowyMobileStateContainer.error( - emoji: '🛸', - title: LocaleKeys.settings_mobile_userprofileError.tr(), - description: LocaleKeys.settings_mobile_userprofileErrorDescription.tr(), - errorMsg: errorMsg, - ); - } - - 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), - ], - ), - ), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 73da7594a7..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ /dev/null @@ -1,267 +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/plugins/trash/application/prelude.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/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:go_router/go_router.dart'; - -class MobileHomeTrashPage extends StatelessWidget { - const MobileHomeTrashPage({super.key}); - - static const routeName = '/trash'; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt()..add(const TrashEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: Text(LocaleKeys.trash_text.tr()), - actions: [ - state.objects.isEmpty - ? const SizedBox.shrink() - : IconButton( - splashRadius: 20, - icon: const Icon(Icons.more_horiz), - onPressed: () { - final trashBloc = context.read(); - showMobileBottomSheet( - context, - showHeader: true, - showCloseButton: true, - showDragHandle: true, - padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), - title: LocaleKeys.trash_mobile_actions.tr(), - builder: (_) => Row( - children: [ - Expanded( - child: _TrashActionAllButton( - trashBloc: trashBloc, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: _TrashActionAllButton( - trashBloc: trashBloc, - type: _TrashActionType.restoreAll, - ), - ), - ], - ), - ); - }, - ), - ], - ), - body: state.objects.isEmpty - ? const _EmptyTrashBin() - : _DeletedFilesListView(state), - ); - }, - ), - ); - } -} - -enum _TrashActionType { - restoreAll, - 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({ - this.type = _TrashActionType.deleteAll, - required this.trashBloc, - }); - final _TrashActionType type; - final TrashBloc trashBloc; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDeleteAll = type == _TrashActionType.deleteAll; - return BlocProvider.value( - value: trashBloc, - child: BottomSheetActionWidget( - svg: isDeleteAll ? FlowySvgs.m_delete_m : FlowySvgs.m_restore_m, - text: isDeleteAll - ? LocaleKeys.trash_deleteAll.tr() - : LocaleKeys.trash_restoreAll.tr(), - onTap: () { - final trashList = trashBloc.state.objects; - if (trashList.isNotEmpty) { - context.pop(); - showFlowyMobileConfirmDialog( - context, - title: FlowyText( - isDeleteAll - ? LocaleKeys.trash_confirmDeleteAll_title.tr() - : LocaleKeys.trash_restoreAll.tr(), - ), - content: FlowyText( - isDeleteAll - ? LocaleKeys.trash_confirmDeleteAll_caption.tr() - : LocaleKeys.trash_confirmRestoreAll_caption.tr(), - ), - actionButtonTitle: isDeleteAll - ? LocaleKeys.trash_deleteAll.tr() - : LocaleKeys.trash_restoreAll.tr(), - actionButtonColor: isDeleteAll - ? theme.colorScheme.error - : theme.colorScheme.primary, - onActionButtonPressed: () { - if (isDeleteAll) { - trashBloc.add( - const TrashEvent.deleteAll(), - ); - } else { - trashBloc.add( - const TrashEvent.restoreAll(), - ); - } - }, - cancelButtonTitle: LocaleKeys.button_cancel.tr(), - ); - } else { - // when there is no deleted files - // show toast - Fluttertoast.showToast( - msg: LocaleKeys.trash_mobile_empty.tr(), - gravity: ToastGravity.CENTER, - ); - } - }, - ), - ); - } -} - -class _DeletedFilesListView extends StatelessWidget { - const _DeletedFilesListView( - this.state, - ); - - final TrashState state; - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ListView.builder( - itemBuilder: (context, index) { - final deletedFile = state.objects[index]; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: ListTile( - // TODO: show different file type icon, implement this feature after TrashPB has file type field - leading: FlowySvg( - FlowySvgs.document_s, - size: const Size.square(24), - color: theme.colorScheme.onSurface, - ), - title: Text( - deletedFile.name, - style: theme.textTheme.labelMedium - ?.copyWith(color: theme.colorScheme.onSurface), - ), - horizontalTitleGap: 0, - tileColor: theme.colorScheme.onSurface.withValues(alpha: 0.1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - splashRadius: 20, - icon: FlowySvg( - FlowySvgs.m_restore_m, - size: const Size.square(24), - color: theme.colorScheme.onSurface, - ), - onPressed: () { - context - .read() - .add(TrashEvent.putback(deletedFile.id)); - Fluttertoast.showToast( - msg: - '${deletedFile.name} ${LocaleKeys.trash_mobile_isRestored.tr()}', - gravity: ToastGravity.BOTTOM, - ); - }, - ), - IconButton( - splashRadius: 20, - icon: FlowySvg( - FlowySvgs.m_delete_m, - size: const Size.square(24), - color: theme.colorScheme.onSurface, - ), - onPressed: () { - context - .read() - .add(TrashEvent.delete(deletedFile)); - Fluttertoast.showToast( - msg: - '${deletedFile.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}', - gravity: ToastGravity.BOTTOM, - ); - }, - ), - ], - ), - ), - ); - }, - itemCount: state.objects.length, - ), - ); - } -} 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 deleted file mode 100644 index 661a422e0c..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.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/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/workspace/application/recent/prelude.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.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: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 MobileRecentFolder extends StatefulWidget { - const MobileRecentFolder({super.key}); - - @override - State createState() => _MobileRecentFolderState(); -} - -class _MobileRecentFolderState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - RecentViewsBloc()..add(const RecentViewsEvent.initial()), - child: BlocListener( - listenWhen: (previous, current) => - current.currentWorkspace != null && - previous.currentWorkspace?.workspaceId != - current.currentWorkspace!.workspaceId, - listener: (context, state) => context - .read() - .add(const RecentViewsEvent.resetRecentViews()), - child: BlocBuilder( - builder: (context, state) { - final ids = {}; - - List recentViews = state.views.map((e) => e.item).toList(); - recentViews.retainWhere((element) => ids.add(element.id)); - - // only keep the first 20 items. - recentViews = recentViews.take(20).toList(); - - if (recentViews.isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - _RecentViews( - key: ValueKey(recentViews), - // the recent views are in reverse order - recentViews: recentViews, - ), - const VSpace(12.0), - ], - ); - }, - ), - ), - ); - } -} - -class _RecentViews extends StatelessWidget { - const _RecentViews({ - super.key, - required this.recentViews, - }); - - final List recentViews; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: GestureDetector( - child: FlowyText.semibold( - LocaleKeys.sideBar_recent.tr(), - fontSize: 20.0, - ), - onTap: () { - showMobileBottomSheet( - context, - showDivider: false, - showDragHandle: true, - backgroundColor: AFThemeExtension.of(context).background, - builder: (_) { - return Column( - children: [ - FlowyOptionTile.text( - text: LocaleKeys.button_clear.tr(), - leftIcon: FlowySvg( - FlowySvgs.m_delete_s, - color: Theme.of(context).colorScheme.error, - ), - textColor: Theme.of(context).colorScheme.error, - onTap: () { - context.read().add( - RecentViewsEvent.removeRecentViews( - recentViews.map((e) => e.id).toList(), - ), - ); - context.pop(); - }, - ), - ], - ); - }, - ); - }, - ), - ), - SizedBox( - height: 148, - child: ListView.separated( - key: const PageStorageKey('recent_views_page_storage_key'), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - final view = recentViews[index]; - return SizedBox.square( - dimension: 148, - child: MobileRecentView(view: view), - ); - }, - separatorBuilder: (context, index) => const HSpace(8), - itemCount: recentViews.length, - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index 966b1ac61a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart +++ /dev/null @@ -1,229 +0,0 @@ -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/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/util/string_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: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 'package:string_validator/string_validator.dart'; - -class MobileRecentView extends StatelessWidget { - const MobileRecentView({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return BlocProvider( - create: (context) => RecentViewBloc(view: view) - ..add( - const RecentViewEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return GestureDetector( - onTap: () => context.pushView(view), - child: Stack( - children: [ - DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: theme.colorScheme.outline), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded(child: _buildCover(context, state)), - Expanded(child: _buildTitle(context, state)), - ], - ), - ), - Align( - alignment: Alignment.centerLeft, - child: _buildIcon(context, state), - ), - ], - ), - ); - }, - ), - ); - } - - Widget _buildCover(BuildContext context, RecentViewState state) { - return Padding( - padding: const EdgeInsets.only(top: 1.0, left: 1.0, right: 1.0), - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - child: _RecentCover( - coverTypeV1: state.coverTypeV1, - coverTypeV2: state.coverTypeV2, - value: state.coverValue, - ), - ), - ); - } - - Widget _buildTitle(BuildContext context, RecentViewState state) { - return Padding( - padding: const EdgeInsets.fromLTRB(8, 18, 8, 2), - // hack: minLines currently not supported in Text widget. - // https://github.com/flutter/flutter/issues/31134 - child: Stack( - children: [ - FlowyText.medium( - view.name, - fontSize: 16.0, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const FlowyText( - "\n\n", - maxLines: 2, - ), - ], - ), - ); - } - - Widget _buildIcon(BuildContext context, RecentViewState state) { - return Padding( - padding: const EdgeInsets.only(left: 8.0), - child: state.icon.isNotEmpty - ? RawEmojiIconWidget(emoji: state.icon, emojiSize: 30) - : SizedBox.square( - dimension: 32.0, - child: view.defaultIcon(), - ), - ); - } -} - -class _RecentCover extends StatelessWidget { - const _RecentCover({ - required this.coverTypeV1, - this.coverTypeV2, - this.value, - }); - - final CoverType coverTypeV1; - final PageStyleCoverImageType? coverTypeV2; - final String? value; - - @override - 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), - ); - final value = this.value; - if (value == null) { - return placeholder; - } - if (coverTypeV2 != null) { - return _buildCoverV2(context, value, placeholder); - } - return _buildCoverV1(context, value, placeholder); - } - - Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) { - final type = coverTypeV2; - if (type == null) { - return placeholder; - } - if (type == PageStyleCoverImageType.customImage || - type == PageStyleCoverImageType.unsplashImage) { - final userProfilePB = Provider.of(context); - return FlowyNetworkImage( - url: value, - userProfilePB: userProfilePB, - ); - } - - if (type == PageStyleCoverImageType.builtInImage) { - return Image.asset( - PageStyleCoverImageType.builtInImagePath(value), - fit: BoxFit.cover, - ); - } - - if (type == PageStyleCoverImageType.pureColor) { - final color = value.coverColor(context); - if (color != null) { - return ColoredBox( - color: color, - ); - } - } - - if (type == PageStyleCoverImageType.gradientColor) { - return Container( - decoration: BoxDecoration( - gradient: FlowyGradientColor.fromId(value).linear, - ), - ); - } - - if (type == PageStyleCoverImageType.localImage) { - return Image.file( - File(value), - fit: BoxFit.cover, - ); - } - - return placeholder; - } - - Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) { - switch (coverTypeV1) { - case CoverType.file: - if (isURL(value)) { - final userProfilePB = Provider.of(context); - return FlowyNetworkImage( - url: value, - userProfilePB: userProfilePB, - ); - } - final imageFile = File(value); - if (!imageFile.existsSync()) { - return placeholder; - } - return Image.file( - imageFile, - ); - case CoverType.asset: - return Image.asset( - value, - fit: BoxFit.cover, - ); - case CoverType.color: - final color = value.tryToColor() ?? Colors.white; - return Container( - color: color, - ); - case CoverType.none: - return placeholder; - } - } -} 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 deleted file mode 100644 index c0baa641d9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart'; -import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/recent/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/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:flutter_slidable/flutter_slidable.dart'; - -class MobileRecentSpace extends StatefulWidget { - const MobileRecentSpace({super.key}); - - @override - State createState() => _MobileRecentSpaceState(); -} - -class _MobileRecentSpaceState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return BlocProvider( - create: (context) => - RecentViewsBloc()..add(const RecentViewsEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const SizedBox.shrink(); - } - - final recentViews = _filterRecentViews(state.views); - - if (recentViews.isEmpty) { - return const Center( - child: EmptySpacePlaceholder(type: MobilePageCardType.recent), - ); - } - - return _RecentViews(recentViews: recentViews); - }, - ), - ); - } - - List _filterRecentViews(List recentViews) { - final ids = {}; - final filteredRecentViews = recentViews.toList(); - filteredRecentViews.retainWhere((e) => ids.add(e.item.id)); - return filteredRecentViews; - } -} - -class _RecentViews extends StatelessWidget { - const _RecentViews({ - required this.recentViews, - }); - - final List recentViews; - - @override - Widget build(BuildContext context) { - final borderColor = Theme.of(context).isLightMode - ? 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: 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 deleted file mode 100644 index 0079ed319a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart +++ /dev/null @@ -1,132 +0,0 @@ -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/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'; -import 'package:appflowy/workspace/presentation/home/home_sizes.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 MobileSectionFolder extends StatelessWidget { - const MobileSectionFolder({ - super.key, - required this.title, - required this.views, - required this.spaceType, - }); - - final String title; - final List views; - final FolderSpaceType spaceType; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FolderBloc(type: spaceType) - ..add( - const FolderEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - SizedBox( - height: HomeSpaceViewSizes.mViewHeight, - child: MobileSectionFolderHeader( - title: title, - isExpanded: context.read().state.isExpanded, - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => _createNewPage(context), - ), - ), - if (state.isExpanded) - Padding( - padding: const EdgeInsets.only( - left: HomeSpaceViewSizes.leftPadding, - ), - child: _Pages( - views: views, - spaceType: spaceType, - ), - ), - ], - ); - }, - ), - ); - } - - 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 deleted file mode 100644 index b1d2bf6909..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -@visibleForTesting -const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey'); - -class MobileSectionFolderHeader extends StatefulWidget { - const MobileSectionFolderHeader({ - super.key, - required this.title, - required this.onPressed, - required this.onAdded, - required this.isExpanded, - }); - - final String title; - final VoidCallback onPressed; - final VoidCallback onAdded; - final bool isExpanded; - - @override - State createState() => - _MobileSectionFolderHeaderState(); -} - -class _MobileSectionFolderHeaderState extends State { - double _turns = 0; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const HSpace(HomeSpaceViewSizes.mHorizontalPadding), - Expanded( - child: FlowyButton( - text: FlowyText.medium( - widget.title, - fontSize: 16.0, - ), - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0), - expandText: false, - iconPadding: 2, - mainAxisAlignment: MainAxisAlignment.start, - rightIcon: AnimatedRotation( - duration: const Duration(milliseconds: 200), - turns: _turns, - child: const FlowySvg( - FlowySvgs.m_spaces_expand_s, - ), - ), - onTap: () { - setState(() { - _turns = widget.isExpanded ? -0.25 : 0; - }); - widget.onPressed(); - }, - ), - ), - 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, - ), - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index 2de40600f2..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.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/mobile/presentation/home/shared/mobile_page_card.dart'; -import 'package:easy_localization/easy_localization.dart'; -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, - }); - - final MobilePageCardType type; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 48.0), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg( - FlowySvgs.m_empty_page_xl, - ), - const VSpace(16.0), - FlowyText.medium( - _emptyPageText, - fontSize: 18.0, - textAlign: TextAlign.center, - ), - const VSpace(8.0), - FlowyText.regular( - _emptyPageSubText, - fontSize: 17.0, - maxLines: 10, - textAlign: TextAlign.center, - lineHeight: 1.3, - color: Theme.of(context).hintColor, - ), - const VSpace(kBottomNavigationBarHeight + 36.0), - ], - ), - ); - } - - String get _emptyPageText => switch (type) { - MobilePageCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(), - MobilePageCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(), - }; - - String get _emptyPageSubText => switch (type) { - MobilePageCardType.recent => - LocaleKeys.sideBar_emptyRecentDescription.tr(), - MobilePageCardType.favorite => - LocaleKeys.sideBar_emptyFavoriteDescription.tr(), - }; -} 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 deleted file mode 100644 index 87ce41d5b6..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart +++ /dev/null @@ -1,419 +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/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'; -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_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:provider/provider.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:time/time.dart'; - -enum MobilePageCardType { - recent, - favorite; - - String get lastOperationHintText => switch (this) { - MobilePageCardType.recent => LocaleKeys.sideBar_lastViewed.tr(), - MobilePageCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(), - }; -} - -class MobileViewPage extends StatelessWidget { - const MobileViewPage({ - super.key, - required this.view, - this.timestamp, - required this.type, - }); - - final ViewPB view; - final Int64? timestamp; - final MobilePageCardType type; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => ViewBloc(view: view, shouldLoadChildViews: false) - ..add(const ViewEvent.initial()), - ), - BlocProvider( - create: (context) => - RecentViewBloc(view: view)..add(const RecentViewEvent.initial()), - ), - ], - child: BlocBuilder( - builder: (context, state) { - return Slidable( - endActionPane: buildEndActionPane( - context, - [ - MobilePaneActionType.more, - context.watch().state.view.isFavorite - ? MobilePaneActionType.removeFromFavorites - : 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: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HSpace(HomeSpaceViewSizes.mHorizontalPadding), - Expanded(child: _buildDescription(context, state)), - const HSpace(20.0), - SizedBox( - width: 84, - height: 60, - child: _buildCover(context, state), - ), - const HSpace(HomeSpaceViewSizes.mHorizontalPadding), - ], - ), - ), - ); - }, - ), - ); - } - - Widget _buildDescription(BuildContext context, RecentViewState state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // page icon & page title - _buildTitle(context, state), - const VSpace(12.0), - // author & last viewed - _buildNameAndLastViewed(context, state), - ], - ); - } - - Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) { - final supportAvatar = isURL(state.icon.emoji); - if (!supportAvatar) { - return _buildLastViewed(context); - } - return Row( - children: [ - _buildAvatar(context, state), - Flexible(child: _buildAuthor(context, state)), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 3.0), - child: FlowySvg(FlowySvgs.dot_s), - ), - _buildLastViewed(context), - ], - ); - } - - Widget _buildAvatar(BuildContext context, RecentViewState state) { - final userProfile = Provider.of(context); - final iconUrl = userProfile?.iconUrl; - if (iconUrl == null || - iconUrl.isEmpty || - view.createdBy != userProfile?.id || - !isURL(iconUrl)) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.only(top: 2, bottom: 2, right: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: SizedBox.square( - dimension: 16.0, - child: FlowyNetworkImage( - url: iconUrl, - ), - ), - ), - ); - } - - Widget _buildCover(BuildContext context, RecentViewState state) { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: _ViewCover( - layout: view.layout, - coverTypeV1: state.coverTypeV1, - coverTypeV2: state.coverTypeV2, - value: state.coverValue, - ), - ); - } - - Widget _buildTitle(BuildContext context, RecentViewState state) { - final name = state.name; - final icon = state.icon; - 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, - ), - ), - ), - const WidgetSpan(child: HSpace(8.0)), - ], - TextSpan( - text: name, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 16.0, - fontWeight: FontWeight.w600, - height: 1.3, - ), - ), - ], - ), - ); - } - - Widget _buildAuthor(BuildContext context, RecentViewState state) { - return FlowyText.regular( - // view.createdBy.toString(), - '', - fontSize: 12.0, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ); - } - - Widget _buildLastViewed(BuildContext context) { - final textColor = Theme.of(context).isLightMode - ? const Color(0x7F171717) - : Colors.white.withValues(alpha: 0.45); - if (timestamp == null) { - return const SizedBox.shrink(); - } - final date = _formatTimestamp( - context, - timestamp!.toInt() * 1000, - ); - return FlowyText.regular( - date, - fontSize: 13.0, - color: textColor, - ); - } - - String _formatTimestamp(BuildContext context, int timestamp) { - final now = DateTime.now(); - final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); - final difference = now.difference(dateTime); - final String date; - - final dateFormate = - context.read().state.dateFormat; - final timeFormate = - 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 = timeFormate.formatTime(dateTime); - } else { - date = dateFormate.formatDate(dateTime, false); - } - - if (difference.inHours >= 1) { - return '${type.lastOperationHintText} $date'; - } - - return date; - } -} - -class _ViewCover extends StatelessWidget { - const _ViewCover({ - required this.layout, - required this.coverTypeV1, - this.coverTypeV2, - this.value, - }); - - final ViewLayoutPB layout; - final CoverType coverTypeV1; - final PageStyleCoverImageType? coverTypeV2; - final String? value; - - @override - Widget build(BuildContext context) { - final placeholder = _buildPlaceholder(context); - final value = this.value; - if (value == null) { - return placeholder; - } - if (coverTypeV2 != null) { - return _buildCoverV2(context, value, placeholder); - } - return _buildCoverV1(context, value, placeholder); - } - - Widget _buildPlaceholder(BuildContext context) { - final isLightMode = Theme.of(context).isLightMode; - final (svg, color) = switch (layout) { - ViewLayoutPB.Document => ( - FlowySvgs.m_document_thumbnail_m, - isLightMode ? const Color(0xCCEDFBFF) : const Color(0x33658B90) - ), - ViewLayoutPB.Grid => ( - FlowySvgs.m_grid_thumbnail_m, - isLightMode ? const Color(0xFFF5F4FF) : const Color(0x338B80AD) - ), - ViewLayoutPB.Board => ( - FlowySvgs.m_board_thumbnail_m, - isLightMode ? const Color(0x7FE0FDD9) : const Color(0x3372936B), - ), - ViewLayoutPB.Calendar => ( - FlowySvgs.m_calendar_thumbnail_m, - isLightMode ? const Color(0xFFFFF7F0) : const Color(0x33A68B77) - ), - ViewLayoutPB.Chat => ( - FlowySvgs.m_chat_thumbnail_m, - isLightMode ? const Color(0x66FFE6FD) : const Color(0x33987195) - ), - _ => ( - FlowySvgs.m_document_thumbnail_m, - isLightMode ? Colors.black : Colors.white - ) - }; - return ColoredBox( - color: color, - child: Center( - child: FlowySvg( - svg, - blendMode: null, - ), - ), - ); - } - - Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) { - final type = coverTypeV2; - if (type == null) { - return placeholder; - } - if (type == PageStyleCoverImageType.customImage || - type == PageStyleCoverImageType.unsplashImage) { - final userProfilePB = Provider.of(context); - return FlowyNetworkImage( - url: value, - userProfilePB: userProfilePB, - ); - } - - if (type == PageStyleCoverImageType.builtInImage) { - return Image.asset( - PageStyleCoverImageType.builtInImagePath(value), - fit: BoxFit.cover, - ); - } - - if (type == PageStyleCoverImageType.pureColor) { - final color = value.coverColor(context); - if (color != null) { - return ColoredBox( - color: color, - ); - } - } - - if (type == PageStyleCoverImageType.gradientColor) { - return Container( - decoration: BoxDecoration( - gradient: FlowyGradientColor.fromId(value).linear, - ), - ); - } - - if (type == PageStyleCoverImageType.localImage) { - return Image.file( - File(value), - fit: BoxFit.cover, - ); - } - - return placeholder; - } - - Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) { - switch (coverTypeV1) { - case CoverType.file: - if (isURL(value)) { - final userProfilePB = Provider.of(context); - return FlowyNetworkImage( - url: value, - userProfilePB: userProfilePB, - ); - } - final imageFile = File(value); - if (!imageFile.existsSync()) { - return placeholder; - } - return Image.file( - imageFile, - ); - case CoverType.asset: - return Image.asset( - value, - fit: BoxFit.cover, - ); - case CoverType.color: - final color = value.tryToColor() ?? Colors.white; - return Container( - color: color, - ); - case CoverType.none: - return placeholder; - } - } -} 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 deleted file mode 100644 index 3bb62a92c8..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space.dart +++ /dev/null @@ -1,185 +0,0 @@ -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/home/space/mobile_space_header.dart'; -import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart'; -import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/shared/list_extension.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -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, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.spaces.isEmpty) { - return const SizedBox.shrink(); - } - - final currentSpace = state.currentSpace ?? state.spaces.first; - - return Column( - children: [ - MobileSpaceHeader( - isExpanded: state.isExpanded, - space: currentSpace, - onAdded: () => _showCreatePageMenu(context, currentSpace), - onPressed: () => _showSpaceMenu(context), - ), - Padding( - padding: const EdgeInsets.only( - left: HomeSpaceViewSizes.mHorizontalPadding, - ), - child: _Pages( - key: ValueKey(currentSpace.id), - space: currentSpace, - ), - ), - ], - ); - }, - ); - } - - void _showSpaceMenu(BuildContext context) { - showMobileBottomSheet( - context, - showDivider: false, - showHeader: true, - 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(), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: MobileSpaceMenu(), - ), - ); - }, - ); - } - - 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), - ); - }, - ); - }, - ); - } -} - -class _Pages extends StatelessWidget { - const _Pages({ - super.key, - required this.space, - }); - - final ViewPB space; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - ViewBloc(view: space)..add(const ViewEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - 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 - .map( - (view) => MobileViewItem( - key: ValueKey( - '${space.id} ${view.id}', - ), - spaceType: spaceType, - isFirstChild: view.id == state.view.childViews.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; - final actions = [ - MobilePaneActionType.more, - if (view.layout == ViewLayoutPB.Document) - MobilePaneActionType.add, - ]; - return buildEndActionPane( - context, - actions, - spaceType: spaceType, - spaceRatio: actions.length == 1 ? 3 : 4, - ); - }, - ), - ) - .toList(), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 0cd80ff1bb..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_header.dart +++ /dev/null @@ -1,143 +0,0 @@ -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'; -import 'package:flutter/material.dart'; - -@visibleForTesting -const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey'); - -class MobileSpaceHeader extends StatelessWidget { - const MobileSpaceHeader({ - super.key, - required this.space, - required this.onPressed, - required this.onAdded, - required this.isExpanded, - }); - - final ViewPB space; - final VoidCallback onPressed; - final VoidCallback onAdded; - final bool isExpanded; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onPressed, - child: SizedBox( - 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), - FlowyText.medium( - space.name, - lineHeight: 1.15, - fontSize: 16.0, - ), - const HSpace(4.0), - const FlowySvg( - FlowySvgs.workspace_drop_down_menu_show_s, - ), - const Spacer(), - 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, - ), - ), - ), - ], - ), - ), - ); - } - - // Future _onAction(SpaceMoreActionType type, dynamic data) async { - // switch (type) { - // case SpaceMoreActionType.rename: - // await _showRenameDialog(); - // break; - // case SpaceMoreActionType.changeIcon: - // final (String icon, String iconColor) = data; - // context.read().add(SpaceEvent.changeIcon(icon, iconColor)); - // break; - // case SpaceMoreActionType.manage: - // _showManageSpaceDialog(context); - // break; - // case SpaceMoreActionType.addNewSpace: - // break; - // case SpaceMoreActionType.collapseAllPages: - // break; - // case SpaceMoreActionType.delete: - // _showDeleteSpaceDialog(context); - // break; - // case SpaceMoreActionType.divider: - // break; - // } - // } - - // Future _showRenameDialog() async { - // await NavigatorTextFieldDialog( - // title: LocaleKeys.space_rename.tr(), - // value: space.name, - // autoSelectAllText: true, - // onConfirm: (name, _) { - // context.read().add(SpaceEvent.rename(space, name)); - // }, - // ).show(context); - // } - - // void _showManageSpaceDialog(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 ManageSpacePopup(), - // ), - // ); - // }, - // ); - // } - - // void _showDeleteSpaceDialog(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 SizedBox(width: 440, child: DeleteSpacePopup()), - // ), - // ); - // }, - // ); - // } -} 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 deleted file mode 100644 index 485e07a28c..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart +++ /dev/null @@ -1,496 +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/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_bloc/flutter_bloc.dart'; - -import 'constants.dart'; -import 'manage_space_widget.dart'; - -class MobileSpaceMenu extends StatelessWidget { - 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, - ), - ), - const SizedBox( - height: SpaceUIConstants.itemHeight, - child: _CreateSpaceButton(), - ), - ], - ), - ); - }, - ); - } -} - -class MobileSpaceMenuItem extends StatelessWidget { - const MobileSpaceMenuItem({ - super.key, - required this.space, - required this.isSelected, - }); - - final ViewPB space; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return FlowyButton( - text: Row( - children: [ - FlowyText.medium( - space.name, - fontSize: 16.0, - ), - const HSpace(6.0), - if (space.spacePermission == SpacePermission.private) - const FlowySvg( - FlowySvgs.space_lock_s, - size: Size.square(12), - ), - ], - ), - 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, - ), - onTap: () { - context.read().add(SpaceEvent.open(space)); - Navigator.of(context).pop(); - }, - ); - } -} - -class _CreateSpaceButton extends StatefulWidget { - const _CreateSpaceButton(); - - @override - State<_CreateSpaceButton> createState() => _CreateSpaceButtonState(); -} - -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, - ); - } -} 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/_round_underline_tab_indicator.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart deleted file mode 100644 index 1a3eb121f3..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; - -class RoundUnderlineTabIndicator extends Decoration { - const RoundUnderlineTabIndicator({ - this.borderRadius, - this.borderSide = const BorderSide(width: 2.0, color: Colors.white), - this.insets = EdgeInsets.zero, - required this.width, - }); - - final BorderRadius? borderRadius; - final BorderSide borderSide; - final EdgeInsetsGeometry insets; - final double width; - - @override - Decoration? lerpFrom(Decoration? a, double t) { - if (a is UnderlineTabIndicator) { - return UnderlineTabIndicator( - borderSide: BorderSide.lerp(a.borderSide, borderSide, t), - insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!, - ); - } - return super.lerpFrom(a, t); - } - - @override - Decoration? lerpTo(Decoration? b, double t) { - if (b is UnderlineTabIndicator) { - return UnderlineTabIndicator( - borderSide: BorderSide.lerp(borderSide, b.borderSide, t), - insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!, - ); - } - return super.lerpTo(b, t); - } - - @override - BoxPainter createBoxPainter([VoidCallback? onChanged]) { - return _UnderlinePainter(this, borderRadius, onChanged); - } - - Rect _indicatorRectFor(Rect rect, TextDirection textDirection) { - final Rect indicator = insets.resolve(textDirection).deflateRect(rect); - final center = indicator.center.dx; - return Rect.fromLTWH( - center - width / 2.0, - indicator.bottom - borderSide.width, - width, - borderSide.width, - ); - } - - @override - Path getClipPath(Rect rect, TextDirection textDirection) { - if (borderRadius != null) { - return Path() - ..addRRect( - borderRadius!.toRRect(_indicatorRectFor(rect, textDirection)), - ); - } - return Path()..addRect(_indicatorRectFor(rect, textDirection)); - } -} - -class _UnderlinePainter extends BoxPainter { - _UnderlinePainter( - this.decoration, - this.borderRadius, - super.onChanged, - ); - - final RoundUnderlineTabIndicator decoration; - final BorderRadius? borderRadius; - - @override - void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { - assert(configuration.size != null); - final Rect rect = offset & configuration.size!; - final TextDirection textDirection = configuration.textDirection!; - final Paint paint; - if (borderRadius != null) { - paint = Paint()..color = decoration.borderSide.color; - final Rect indicator = decoration._indicatorRectFor(rect, textDirection); - final RRect rrect = RRect.fromRectAndCorners( - indicator, - topLeft: borderRadius!.topLeft, - topRight: borderRadius!.topRight, - bottomRight: borderRadius!.bottomRight, - bottomLeft: borderRadius!.bottomLeft, - ); - canvas.drawRRect(rrect, paint); - } else { - paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.round; - final Rect indicator = decoration - ._indicatorRectFor(rect, textDirection) - .deflate(decoration.borderSide.width / 2.0); - canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint); - } - } -} 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 deleted file mode 100644 index f8c9a0d3b1..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; -import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:reorderable_tabbar/reorderable_tabbar.dart'; - -class MobileSpaceTabBar extends StatelessWidget { - const MobileSpaceTabBar({ - super.key, - this.height = 38.0, - required this.tabController, - required this.tabs, - required this.onReorder, - }); - - final double height; - final List tabs; - final TabController tabController; - final OnReorder onReorder; - - @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, - indicatorColor: Theme.of(context).primaryColor, - isScrollable: true, - labelStyle: labelStyle, - labelColor: baseStyle?.color, - labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), - unselectedLabelStyle: unselectedLabelStyle, - overlayColor: WidgetStateProperty.all(Colors.transparent), - indicator: RoundUnderlineTabIndicator( - width: 28.0, - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - width: 3, - ), - ), - onReorder: onReorder, - ), - ); - } -} 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 deleted file mode 100644 index 56f5f3e6ab..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ /dev/null @@ -1,221 +0,0 @@ -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, - }); - - final UserProfilePB userProfile; - - @override - State createState() => _MobileSpaceTabState(); -} - -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(); - } - - @override - 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(); - } - - _initTabController(state); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MobileSpaceTabBar( - tabController: tabController!, - tabs: state.tabsOrder, - onReorder: (from, to) { - context.read().add( - SpaceOrderEvent.reorder(from, to), - ); - }, - ), - const HSpace(12.0), - Expanded( - child: TabBarView( - controller: tabController, - children: _buildTabs(state), - ), - ), - ], - ); - }, - ), - ), - ); - } - - void _initTabController(SpaceOrderState state) { - if (tabController != null) { - return; - } - tabController = TabController( - length: state.tabsOrder.length, - vsync: this, - initialIndex: state.tabsOrder.indexOf(state.defaultTab), - ); - tabController?.addListener(_onTabChange); - } - - void _onTabChange() { - if (tabController == null) { - return; - } - context - .read() - .add(SpaceOrderEvent.open(tabController!.index)); - } - - List _buildTabs(SpaceOrderState state) { - return state.tabsOrder.map((tab) { - switch (tab) { - 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(), - ), - ], - ); - case MobileSpaceTabType.favorites: - return MobileFavoriteSpace(userProfile: widget.userProfile); - } - }).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/tab/space_order_bloc.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart deleted file mode 100644 index e3c1439dd4..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart +++ /dev/null @@ -1,127 +0,0 @@ -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/startup/startup.dart'; -import 'package:bloc/bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'space_order_bloc.freezed.dart'; - -enum MobileSpaceTabType { - // DO NOT CHANGE THE ORDER - spaces, - recent, - favorites; - - String get tr { - switch (this) { - case MobileSpaceTabType.recent: - return LocaleKeys.sideBar_RecentSpace.tr(); - case MobileSpaceTabType.spaces: - return LocaleKeys.sideBar_Spaces.tr(); - case MobileSpaceTabType.favorites: - return LocaleKeys.sideBar_favoriteSpace.tr(); - } - } -} - -class SpaceOrderBloc extends Bloc { - SpaceOrderBloc() : super(const SpaceOrderState()) { - on( - (event, emit) async { - await event.when( - initial: () async { - final tabsOrder = await _getTabsOrder(); - final defaultTab = await _getDefaultTab(); - emit( - state.copyWith( - tabsOrder: tabsOrder, - defaultTab: defaultTab, - isLoading: false, - ), - ); - }, - open: (index) async { - final tab = state.tabsOrder[index]; - await _setDefaultTab(tab); - }, - reorder: (from, to) async { - final tabsOrder = List.of(state.tabsOrder); - tabsOrder.insert(to, tabsOrder.removeAt(from)); - await _setTabsOrder(tabsOrder); - emit(state.copyWith(tabsOrder: tabsOrder)); - }, - ); - }, - ); - } - - final _storage = getIt(); - - Future _getDefaultTab() async { - try { - return await _storage.getWithFormat( - KVKeys.lastOpenedSpace, (value) { - return MobileSpaceTabType.values[int.parse(value)]; - }) ?? - MobileSpaceTabType.spaces; - } catch (e) { - return MobileSpaceTabType.spaces; - } - } - - Future _setDefaultTab(MobileSpaceTabType tab) async { - await _storage.set( - KVKeys.lastOpenedSpace, - tab.index.toString(), - ); - } - - Future> _getTabsOrder() async { - try { - return await _storage.getWithFormat>( - KVKeys.spaceOrder, (value) { - final order = jsonDecode(value).cast(); - if (order.isEmpty) { - return MobileSpaceTabType.values; - } - return order - .map((e) => MobileSpaceTabType.values[e]) - .cast() - .toList(); - }) ?? - MobileSpaceTabType.values; - } catch (e) { - return MobileSpaceTabType.values; - } - } - - Future _setTabsOrder(List tabsOrder) async { - await _storage.set( - KVKeys.spaceOrder, - jsonEncode(tabsOrder.map((e) => e.index).toList()), - ); - } -} - -@freezed -class SpaceOrderEvent with _$SpaceOrderEvent { - const factory SpaceOrderEvent.initial() = Initial; - const factory SpaceOrderEvent.open(int index) = Open; - const factory SpaceOrderEvent.reorder(int from, int to) = Reorder; -} - -@freezed -class SpaceOrderState with _$SpaceOrderState { - const factory SpaceOrderState({ - @Default(MobileSpaceTabType.spaces) MobileSpaceTabType defaultTab, - @Default(MobileSpaceTabType.values) List tabsOrder, - @Default(true) bool isLoading, - }) = _SpaceOrderState; - - factory SpaceOrderState.initial() => const SpaceOrderState(); -} 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 deleted file mode 100644 index d306f48964..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ /dev/null @@ -1,489 +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/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({ - super.key, - required this.userProfile, - required this.currentWorkspace, - required this.workspaces, - required this.onWorkspaceSelected, - }); - - final UserProfilePB userProfile; - final UserWorkspacePB currentWorkspace; - final List workspaces; - final void Function(UserWorkspacePB workspace) onWorkspaceSelected; - - @override - Widget build(BuildContext context) { - // user profile - final List children = [ - _WorkspaceUserItem(userProfile: userProfile), - _buildDivider(), - ]; - - // workspace list - for (var i = 0; i < workspaces.length; i++) { - final workspace = workspaces[i]; - children.add( - _WorkspaceMenuItem( - key: ValueKey(workspace.workspaceId), - userProfile: userProfile, - workspace: workspace, - showTopBorder: false, - currentWorkspace: currentWorkspace, - onWorkspaceSelected: onWorkspaceSelected, - ), - ); - } - - // 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 { - const _WorkspaceUserItem({required this.userProfile}); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final color = Theme.of(context).isLightMode - ? const Color(0x99333333) - : const Color(0x99CCCCCC); - return FlowyOptionTile.text( - height: 32, - showTopBorder: false, - showBottomBorder: false, - content: Expanded( - child: Padding( - padding: const EdgeInsets.only(), - child: FlowyText( - userProfile.email, - fontSize: 14, - color: color, - ), - ), - ), - ); - } -} - -class _WorkspaceMenuItem extends StatelessWidget { - const _WorkspaceMenuItem({ - super.key, - required this.userProfile, - required this.workspace, - required this.showTopBorder, - required this.currentWorkspace, - required this.onWorkspaceSelected, - }); - - final UserProfilePB userProfile; - final UserWorkspacePB workspace; - final bool showTopBorder; - final UserWorkspacePB currentWorkspace; - final void Function(UserWorkspacePB workspace) onWorkspaceSelected; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => WorkspaceMemberBloc( - userProfile: userProfile, - workspace: workspace, - )..add(const WorkspaceMemberEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return FlowyOptionTile.text( - height: 60, - showTopBorder: showTopBorder, - showBottomBorder: false, - leftIcon: _WorkspaceMenuItemIcon(workspace: workspace), - trailing: _WorkspaceMenuItemTrailing( - workspace: workspace, - currentWorkspace: currentWorkspace, - ), - 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 deleted file mode 100644 index 3c6adb8627..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ /dev/null @@ -1,381 +0,0 @@ -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 = - 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); - } -} - -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), - ), - BottomNavigationBarItem( - key: BottomNavigationBarItemType.add.valueKey, - label: BottomNavigationBarItemType.add.label, - icon: const FlowySvg(FlowySvgs.m_home_add_m), - ), - BottomNavigationBarItem( - key: BottomNavigationBarItemType.notification.valueKey, - label: BottomNavigationBarItemType.notification.label, - icon: const _NotificationNavigationBarItemIcon(), - activeIcon: const _NotificationNavigationBarItemIcon( - isActive: true, - ), - ), -]; - -/// Builds the "shell" for the app by building a Scaffold with a -/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. -class MobileBottomNavigationBar extends StatefulWidget { - /// Constructs an [MobileBottomNavigationBar]. - const MobileBottomNavigationBar({ - required this.navigationShell, - super.key, - }); - - /// 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), - }; - - return Scaffold( - body: widget.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, - ), - child: Theme( - data: _getThemeData(context), - child: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - enableFeedback: false, - type: BottomNavigationBarType.fixed, - elevation: 0, - items: _items, - backgroundColor: Colors.transparent, - currentIndex: navigationShell.currentIndex, - onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex), - ), - ), - ), - ), - ); - } - - 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) { - // show an add dialog - mobileCreateNewPageNotifier.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 - // Navigator for the branch is restored. - navigationShell.goBranch( - bottomBarIndex, - // A common pattern when using bottom navigation bars is to support - // navigating to the initial location when tapping the item that is - // already active. This example demonstrates how to support this behavior, - // using the initialLocation parameter of goBranch. - initialLocation: bottomBarIndex == navigationShell.currentIndex, - ); - } -} - -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 deleted file mode 100644 index 33c2eb3905..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/user_profile/user_profile_bloc.dart'; -import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; -import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileNotificationsScreen extends StatefulWidget { - const MobileNotificationsScreen({super.key}); - - static const routeName = '/notifications'; - - @override - State createState() => - _MobileNotificationsScreenState(); -} - -class _MobileNotificationsScreenState extends State - with SingleTickerProviderStateMixin { - final ReminderBloc reminderBloc = getIt(); - late final TabController controller = TabController(length: 2, vsync: this); - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => - UserProfileBloc()..add(const UserProfileEvent.started()), - ), - BlocProvider.value(value: reminderBloc), - BlocProvider( - create: (_) => NotificationFilterBloc(), - ), - ], - child: BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () => - const Center(child: CircularProgressIndicator.adaptive()), - workspaceFailure: () => const WorkspaceFailedScreen(), - success: (workspaceLatest, userProfile) => - _NotificationScreenContent( - workspaceLatest: workspaceLatest, - userProfile: userProfile, - controller: controller, - reminderBloc: reminderBloc, - ), - ); - }, - ), - ); - } -} - -class _NotificationScreenContent extends StatelessWidget { - const _NotificationScreenContent({ - required this.workspaceLatest, - required this.userProfile, - required this.controller, - required this.reminderBloc, - }); - - final WorkspaceLatestPB workspaceLatest; - final UserProfilePB userProfile; - final TabController controller; - final ReminderBloc reminderBloc; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SidebarSectionsBloc() - ..add( - SidebarSectionsEvent.initial( - userProfile, - workspaceLatest.workspaceId, - ), - ), - child: BlocBuilder( - builder: (context, sectionState) => - BlocBuilder( - builder: (context, filterState) => - BlocBuilder( - builder: (context, state) { - // Workaround for rebuilding the Blocks by brightness - Theme.of(context).brightness; - - final List pastReminders = state.pastReminders - .where( - (r) => filterState.showUnreadsOnly ? !r.isRead : true, - ) - .sortByScheduledAt(); - - final List upcomingReminders = - state.upcomingReminders.sortByScheduledAt(); - - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - elevation: 0, - title: Text(LocaleKeys.notificationHub_mobile_title.tr()), - ), - body: SafeArea( - child: Column( - children: [ - MobileNotificationTabBar(controller: controller), - Expanded( - child: TabBarView( - controller: controller, - children: [ - NotificationsView( - shownReminders: pastReminders, - reminderBloc: reminderBloc, - views: sectionState.section.publicViews, - onAction: _onAction, - onReadChanged: _onReadChanged, - actionBar: InboxActionBar( - hasUnreads: state.hasUnreads, - showUnreadsOnly: filterState.showUnreadsOnly, - ), - ), - NotificationsView( - shownReminders: upcomingReminders, - reminderBloc: reminderBloc, - views: sectionState.section.publicViews, - isUpcoming: true, - onAction: _onAction, - ), - ], - ), - ), - ], - ), - ), - ); - }, - ), - ), - ), - ); - } - - void _onAction(ReminderPB reminder, int? path, ViewPB? view) => - reminderBloc.add( - ReminderEvent.pressReminder( - reminderId: reminder.id, - path: path, - view: view, - ), - ); - - 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 deleted file mode 100644 index 59bfe61822..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; - -class MobileNotificationTabBar extends StatefulWidget { - const MobileNotificationTabBar({super.key, required this.controller}); - - final TabController controller; - - @override - State createState() => - _MobileNotificationTabBarState(); -} - -class _MobileNotificationTabBarState extends State { - @override - void initState() { - super.initState(); - widget.controller.addListener(_updateState); - } - - @override - void dispose() { - widget.controller.removeListener(_updateState); - super.dispose(); - } - - void _updateState() => setState(() {}); - - @override - Widget build(BuildContext context) { - final borderSide = BorderSide( - color: AFThemeExtension.of(context).calloutBGColor, - ); - - return DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: borderSide, - top: borderSide, - ), - ), - child: Row( - children: [ - Expanded( - child: TabBar( - controller: widget.controller, - padding: const EdgeInsets.symmetric(horizontal: 8), - labelPadding: EdgeInsets.zero, - indicatorSize: TabBarIndicatorSize.label, - indicator: UnderlineTabIndicator( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - isScrollable: true, - tabs: [ - FlowyTabItem( - label: LocaleKeys.notificationHub_tabs_inbox.tr(), - isSelected: widget.controller.index == 0, - ), - FlowyTabItem( - label: LocaleKeys.notificationHub_tabs_upcoming.tr(), - isSelected: widget.controller.index == 1, - ), - ], - ), - ), - ], - ), - ); - } -} 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_slide_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart deleted file mode 100644 index b3d021613e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; - -class MobileSlideActionButton extends StatelessWidget { - const MobileSlideActionButton({ - super.key, - required this.svg, - this.size = 32.0, - this.backgroundColor = Colors.transparent, - this.borderRadius = BorderRadius.zero, - required this.onPressed, - }); - - final FlowySvgData svg; - final double size; - final Color backgroundColor; - final SlidableActionCallback onPressed; - final BorderRadius borderRadius; - - @override - Widget build(BuildContext context) { - return CustomSlidableAction( - borderRadius: borderRadius, - backgroundColor: backgroundColor, - onPressed: (context) { - HapticFeedback.mediumImpact(); - onPressed(context); - }, - padding: EdgeInsets.zero, - child: FlowySvg( - svg, - size: Size.square(size), - color: Colors.white, - ), - ); - } -} 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 deleted file mode 100644 index 6e2611a684..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ /dev/null @@ -1,353 +0,0 @@ -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'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; - -typedef ViewItemOnSelected = void Function(ViewPB); -typedef ActionPaneBuilder = ActionPane Function(BuildContext context); - -class MobileViewItem extends StatelessWidget { - const MobileViewItem({ - super.key, - required this.view, - this.parentView, - required this.spaceType, - required this.level, - this.leftPadding = 10, - required this.onSelected, - this.isFirstChild = false, - this.isDraggable = true, - required this.isFeedback, - this.startActionPane, - this.endActionPane, - }); - - final ViewPB view; - final ViewPB? parentView; - - final FolderSpaceType spaceType; - - // indicate the level of the view item - // used to calculate the left padding - final int level; - - // the left padding of the view item for each level - // the left padding of the each level = level * leftPadding - final double leftPadding; - - // Selected by normal conventions - final ViewItemOnSelected onSelected; - - // used for indicating the first child of the parent view, so that we can - // add top border to the first child - final bool isFirstChild; - - // it should be false when it's rendered as feedback widget inside DraggableItem - final bool isDraggable; - - // identify if the view item is rendered as feedback widget inside DraggableItem - final bool isFeedback; - - // the actions of the view item, such as favorite, rename, delete, etc. - final ActionPaneBuilder? startActionPane; - final ActionPaneBuilder? endActionPane; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), - child: BlocConsumer( - listenWhen: (p, c) => - c.lastCreatedView != null && - p.lastCreatedView?.id != c.lastCreatedView!.id, - listener: (context, state) => context.pushView(state.lastCreatedView!), - builder: (context, state) { - return InnerMobileViewItem( - view: state.view, - parentView: parentView, - childViews: state.view.childViews, - spaceType: spaceType, - level: level, - leftPadding: leftPadding, - showActions: true, - isExpanded: state.isExpanded, - onSelected: onSelected, - isFirstChild: isFirstChild, - isDraggable: isDraggable, - isFeedback: isFeedback, - startActionPane: startActionPane, - endActionPane: endActionPane, - ); - }, - ), - ); - } -} - -class InnerMobileViewItem extends StatelessWidget { - const InnerMobileViewItem({ - super.key, - required this.view, - required this.parentView, - required this.childViews, - required this.spaceType, - this.isDraggable = true, - this.isExpanded = true, - required this.level, - required this.leftPadding, - required this.showActions, - required this.onSelected, - this.isFirstChild = false, - required this.isFeedback, - this.startActionPane, - this.endActionPane, - }); - - final ViewPB view; - final ViewPB? parentView; - final List childViews; - final FolderSpaceType spaceType; - - final bool isDraggable; - final bool isExpanded; - final bool isFirstChild; - - // identify if the view item is rendered as feedback widget inside DraggableItem - final bool isFeedback; - - final int level; - final double leftPadding; - - final bool showActions; - final ViewItemOnSelected onSelected; - - final ActionPaneBuilder? startActionPane; - final ActionPaneBuilder? endActionPane; - - @override - Widget build(BuildContext context) { - Widget child = SingleMobileInnerViewItem( - view: view, - parentView: parentView, - level: level, - showActions: showActions, - spaceType: spaceType, - onSelected: onSelected, - isExpanded: isExpanded, - isDraggable: isDraggable, - leftPadding: leftPadding, - isFeedback: isFeedback, - startActionPane: startActionPane, - endActionPane: endActionPane, - ); - - // if the view is expanded and has child views, render its child views - if (isExpanded) { - if (childViews.isNotEmpty) { - final children = childViews.map((childView) { - return MobileViewItem( - key: ValueKey('${spaceType.name} ${childView.id}'), - parentView: view, - spaceType: spaceType, - isFirstChild: childView.id == childViews.first.id, - view: childView, - level: level + 1, - onSelected: onSelected, - isDraggable: isDraggable, - leftPadding: leftPadding, - isFeedback: isFeedback, - startActionPane: startActionPane, - endActionPane: endActionPane, - ); - }).toList(); - - child = Column( - mainAxisSize: MainAxisSize.min, - children: [ - child, - ...children, - ], - ); - } - } - - // wrap the child with DraggableItem if isDraggable is true - if (isDraggable && !isReferencedDatabaseView(view, parentView)) { - child = DraggableViewItem( - isFirstChild: isFirstChild, - view: view, - centerHighlightColor: Colors.blue.shade200, - topHighlightColor: Colors.blue.shade200, - bottomHighlightColor: Colors.blue.shade200, - feedback: (context) { - return MobileViewItem( - view: view, - parentView: parentView, - spaceType: spaceType, - level: level, - onSelected: onSelected, - isDraggable: false, - leftPadding: leftPadding, - isFeedback: true, - startActionPane: startActionPane, - endActionPane: endActionPane, - ); - }, - child: child, - ); - } - - return child; - } -} - -class SingleMobileInnerViewItem extends StatefulWidget { - const SingleMobileInnerViewItem({ - super.key, - required this.view, - required this.parentView, - required this.isExpanded, - required this.level, - required this.leftPadding, - this.isDraggable = true, - required this.spaceType, - required this.showActions, - required this.onSelected, - required this.isFeedback, - this.startActionPane, - this.endActionPane, - }); - - final ViewPB view; - final ViewPB? parentView; - final bool isExpanded; - - // identify if the view item is rendered as feedback widget inside DraggableItem - final bool isFeedback; - - final int level; - final double leftPadding; - - final bool isDraggable; - final bool showActions; - final ViewItemOnSelected onSelected; - final FolderSpaceType spaceType; - final ActionPaneBuilder? startActionPane; - final ActionPaneBuilder? endActionPane; - - @override - State createState() => - _SingleMobileInnerViewItemState(); -} - -class _SingleMobileInnerViewItemState extends State { - @override - Widget build(BuildContext context) { - final children = [ - // expand icon - _buildLeftIcon(), - // icon - _buildViewIcon(), - const HSpace(8), - // title - Expanded( - child: FlowyText.regular( - widget.view.nameOrDefault, - fontSize: 16.0, - figmaLineHeight: 20.0, - overflow: TextOverflow.ellipsis, - ), - ), - ]; - - Widget child = InkWell( - borderRadius: BorderRadius.circular(4.0), - onTap: () => widget.onSelected(widget.view), - child: SizedBox( - height: HomeSpaceViewSizes.mViewHeight, - child: Padding( - padding: EdgeInsets.only(left: widget.level * widget.leftPadding), - child: Row( - children: children, - ), - ), - ), - ); - - if (widget.startActionPane != null || widget.endActionPane != null) { - child = Slidable( - // Specify a key if the Slidable is dismissible. - key: ValueKey(widget.view.hashCode), - startActionPane: widget.startActionPane?.call(context), - endActionPane: widget.endActionPane?.call(context), - child: child, - ); - } - - return child; - } - - 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, - ) - : Opacity( - opacity: 0.7, - child: widget.view.defaultIcon(size: const Size.square(18)), - ); - return SizedBox( - width: 18.0, - child: icon, - ); - } - - // > button or · button - // show > if the view is expandable. - // show · if the view can't contain child views. - Widget _buildLeftIcon() { - const rightPadding = 6.0; - if (context.read().state.view.childViews.isEmpty) { - return HSpace(widget.leftPadding + rightPadding); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Padding( - padding: - const EdgeInsets.only(right: rightPadding, top: 6.0, bottom: 6.0), - child: FlowySvg( - widget.isExpanded ? FlowySvgs.m_expand_s : FlowySvgs.m_collapse_s, - blendMode: null, - ), - ), - onTap: () { - context - .read() - .add(ViewEvent.setIsExpanded(!widget.isExpanded)); - }, - ); - } -} - -// workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field. -bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) { - if (parentView == null) { - return false; - } - return view.layout.isDatabaseView && parentView.layout.isDatabaseView; -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart deleted file mode 100644 index 77bb57773f..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileViewAddButton extends StatelessWidget { - const MobileViewAddButton({ - super.key, - required this.onPressed, - }); - - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - width: HomeSpaceViewSizes.mViewButtonDimension, - height: HomeSpaceViewSizes.mViewButtonDimension, - icon: const FlowySvg( - FlowySvgs.m_space_add_s, - ), - onPressed: onPressed, - ); - } -} - -class MobileViewMoreButton extends StatelessWidget { - const MobileViewMoreButton({ - super.key, - required this.onPressed, - }); - - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - width: HomeSpaceViewSizes.mViewButtonDimension, - height: HomeSpaceViewSizes.mViewButtonDimension, - icon: const FlowySvg( - FlowySvgs.m_space_more_s, - ), - onPressed: onPressed, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart b/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart deleted file mode 100644 index 69a3ae503c..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'editor/mobile_editor_screen.dart'; -export 'home/home.dart'; -export 'mobile_bottom_navigation_bar.dart'; -export 'root_placeholder_page.dart'; -export 'setting/setting.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/root_placeholder_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/root_placeholder_page.dart deleted file mode 100644 index 7465ccaa34..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/root_placeholder_page.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -/// Widget for the root/initial pages in the bottom navigation bar. -class RootPlaceholderScreen extends StatelessWidget { - /// Creates a RootScreen - const RootPlaceholderScreen({ - required this.label, - required this.detailsPath, - this.secondDetailsPath, - super.key, - }); - - /// The label - final String label; - - /// The path to the detail page - final String detailsPath; - - /// The path to another detail page - final String? secondDetailsPath; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - centerTitle: true, - title: FlowyText.medium(label), - ), - body: const SizedBox.shrink(), - ); - } -} 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.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about.dart deleted file mode 100644 index dc5d3ea55e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about.dart +++ /dev/null @@ -1 +0,0 @@ -export 'about_setting_group.dart'; 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 deleted file mode 100644 index 2d5a3176cd..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/tasks/device_info_task.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.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:go_router/go_router.dart'; - -import '../widgets/widgets.dart'; - -class AboutSettingGroup extends StatelessWidget { - const AboutSettingGroup({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_mobile_about.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_mobile_privacyPolicy.tr(), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () => afLaunchUrlString('https://appflowy.com/privacy'), - ), - MobileSettingItem( - name: LocaleKeys.settings_mobile_termsAndConditions.tr(), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () => afLaunchUrlString('https://appflowy.com/terms'), - ), - if (kDebugMode) - MobileSettingItem( - name: 'Feature Flags', - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () { - context.push(FeatureFlagScreen.routeName); - }, - ), - MobileSettingItem( - name: LocaleKeys.settings_mobile_version.tr(), - trailing: FlowyText( - '${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})', - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index 47e356e6c1..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/setting/appearance/rtl_setting.dart'; -import 'package:appflowy/mobile/presentation/setting/appearance/text_scale_setting.dart'; -import 'package:appflowy/mobile/presentation/setting/appearance/theme_setting.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import '../setting.dart'; - -class AppearanceSettingGroup extends StatelessWidget { - const AppearanceSettingGroup({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_menu_appearance.tr(), - settingItemList: const [ - ThemeSetting(), - FontSetting(), - DisplaySizeSetting(), - 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 deleted file mode 100644 index 5b8035f004..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart +++ /dev/null @@ -1,100 +0,0 @@ -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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../setting.dart'; - -class RTLSetting extends StatelessWidget { - const RTLSetting({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final textDirection = - context.watch().state.textDirection; - return MobileSettingItem( - name: LocaleKeys.settings_appearance_textDirection_label.tr(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - _textDirectionLabelText(textDirection), - color: theme.colorScheme.onSurface, - ), - const Icon(Icons.chevron_right), - ], - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showDivider: false, - title: LocaleKeys.settings_appearance_textDirection_label.tr(), - builder: (context) { - return Column( - children: [ - FlowyOptionTile.checkbox( - text: LocaleKeys.settings_appearance_textDirection_ltr.tr(), - isSelected: textDirection == AppFlowyTextDirection.ltr, - onTap: () => applyTextDirectionAndPop( - context, - AppFlowyTextDirection.ltr, - ), - ), - 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, - ), - ), - ], - ); - }, - ); - }, - ); - } - - String _textDirectionLabelText(AppFlowyTextDirection textDirection) { - switch (textDirection) { - case AppFlowyTextDirection.auto: - return LocaleKeys.settings_appearance_textDirection_auto.tr(); - case AppFlowyTextDirection.rtl: - return LocaleKeys.settings_appearance_textDirection_rtl.tr(); - case AppFlowyTextDirection.ltr: - 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 deleted file mode 100644 index 7c89185e79..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart +++ /dev/null @@ -1,98 +0,0 @@ -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/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 '../setting.dart'; - -const int _divisions = 4; -const double _minMobileScaleFactor = 0.8; -const double _maxMobileScaleFactor = 1.2; - -class DisplaySizeSetting extends StatefulWidget { - const DisplaySizeSetting({ - 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); - return MobileSettingItem( - name: LocaleKeys.settings_appearance_displaySize.tr(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - scaleFactor.toStringAsFixed(2), - color: theme.colorScheme.onSurface, - ), - const Icon(Icons.chevron_right), - ], - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showDivider: false, - title: LocaleKeys.settings_appearance_displaySize.tr(), - builder: (context) { - return FontSizeStepper( - value: scaleFactor, - minimumValue: _minMobileScaleFactor, - maximumValue: _maxMobileScaleFactor, - divisions: _divisions, - onChanged: (newScaleFactor) async { - await _setScale(newScaleFactor); - }, - ); - }, - ); - }, - ); - } - - 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/appearance/theme_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart deleted file mode 100644 index 8893eab105..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart +++ /dev/null @@ -1,95 +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/util/theme_mode_extension.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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../setting.dart'; - -class ThemeSetting extends StatelessWidget { - const ThemeSetting({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final themeMode = context.watch().state.themeMode; - return MobileSettingItem( - name: LocaleKeys.settings_appearance_themeMode_label.tr(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - themeMode.labelText, - color: theme.colorScheme.onSurface, - ), - const Icon(Icons.chevron_right), - ], - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showDivider: false, - title: LocaleKeys.settings_appearance_themeMode_label.tr(), - builder: (context) { - final themeMode = - context.read().state.themeMode; - return Column( - children: [ - FlowyOptionTile.checkbox( - text: LocaleKeys.settings_appearance_themeMode_system.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_theme_mode_system_s, - ), - isSelected: themeMode == ThemeMode.system, - onTap: () { - context - .read() - .setThemeMode(ThemeMode.system); - Navigator.pop(context); - }, - ), - FlowyOptionTile.checkbox( - showTopBorder: false, - text: LocaleKeys.settings_appearance_themeMode_light.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_theme_mode_light_s, - ), - isSelected: themeMode == ThemeMode.light, - onTap: () { - context - .read() - .setThemeMode(ThemeMode.light); - Navigator.pop(context); - }, - ), - FlowyOptionTile.checkbox( - showTopBorder: false, - text: LocaleKeys.settings_appearance_themeMode_dark.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_theme_mode_dark_s, - ), - isSelected: themeMode == ThemeMode.dark, - onTap: () { - context - .read() - .setThemeMode(ThemeMode.dark); - Navigator.pop(context); - }, - ), - ], - ); - }, - ); - }, - ); - } -} 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 deleted file mode 100644 index 02d620e559..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart +++ /dev/null @@ -1,28 +0,0 @@ -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'; - -class AppFlowyCloudPage extends StatelessWidget { - const AppFlowyCloudPage({super.key}); - - static const routeName = '/AppFlowyCloudPage'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: FlowyAppBar( - titleText: LocaleKeys.settings_menu_cloudSettings.tr(), - ), - body: SettingCloud( - restartAppFlowy: () async { - await getIt().signOut(); - await runAppFlowy(); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart deleted file mode 100644 index 7410554632..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -class CloudSettingGroup extends StatelessWidget { - const CloudSettingGroup({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, snapshot) => MobileSettingGroup( - groupTitle: LocaleKeys.settings_menu_cloudSettings.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_menu_cloudAppFlowy.tr(), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () => context.push(AppFlowyCloudPage.routeName), - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart deleted file mode 100644 index 390f0824de..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.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.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/util/font_family_extension.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:easy_localization/easy_localization.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'; - -final List _availableFonts = [ - defaultFontFamily, - ...GoogleFonts.asMap().keys, -]; - -class FontPickerScreen extends StatelessWidget { - const FontPickerScreen({super.key}); - - static const routeName = '/font_picker'; - - @override - Widget build(BuildContext context) { - return const LanguagePickerPage(); - } -} - -class LanguagePickerPage extends StatefulWidget { - const LanguagePickerPage({super.key}); - - @override - State createState() => _LanguagePickerPageState(); -} - -class _LanguagePickerPageState extends State { - late List availableFonts; - - @override - void initState() { - super.initState(); - availableFonts = _availableFonts; - } - - @override - Widget build(BuildContext context) { - final selectedFontFamilyName = - context.watch().state.font; - return Scaffold( - appBar: FlowyAppBar( - titleText: LocaleKeys.titleBar_font.tr(), - ), - body: SafeArea( - child: Scrollbar( - child: FontSelector( - selectedFontFamilyName: selectedFontFamilyName, - onFontFamilySelected: (fontFamilyName) => - context.pop(fontFamilyName), - ), - ), - ), - ); - } -} - -class FontSelector extends StatefulWidget { - const FontSelector({ - super.key, - this.scrollController, - required this.selectedFontFamilyName, - required this.onFontFamilySelected, - }); - - final ScrollController? scrollController; - final String selectedFontFamilyName; - final void Function(String fontFamilyName) onFontFamilySelected; - - @override - State createState() => _FontSelectorState(); -} - -class _FontSelectorState extends State { - late List availableFonts; - - @override - void initState() { - super.initState(); - availableFonts = _availableFonts; - } - - @override - Widget build(BuildContext context) { - return ListView.builder( - controller: widget.scrollController, - itemCount: availableFonts.length + 1, // with search bar - itemBuilder: (context, index) { - if (index == 0) { - // search bar - return _buildSearchBar(context); - } - - final fontFamilyName = availableFonts[index - 1]; - final usingDefaultFontFamily = fontFamilyName == defaultFontFamily; - final fontFamily = !usingDefaultFontFamily - ? getGoogleFontSafely(fontFamilyName).fontFamily - : defaultFontFamily; - return FlowyOptionTile.checkbox( - text: fontFamilyName.fontFamilyDisplayName, - isSelected: widget.selectedFontFamilyName == fontFamilyName, - showTopBorder: false, - onTap: () => widget.onFontFamilySelected(fontFamilyName), - fontFamily: fontFamily, - backgroundColor: Colors.transparent, - ); - }, - ); - } - - Widget _buildSearchBar(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 12.0, - ), - child: FlowyMobileSearchTextField( - onChanged: (keyword) { - setState(() { - availableFonts = _availableFonts - .where( - (font) => - font.isEmpty || // keep the default one always - font - .parseFontFamilyName() - .toLowerCase() - .contains(keyword.toLowerCase()), - ) - .toList(); - }); - }, - ), - ); - } -} 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 deleted file mode 100644 index 1076b9dba6..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/util/font_family_extension.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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -import '../setting.dart'; - -class FontSetting extends StatelessWidget { - const FontSetting({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final selectedFont = context.watch().state.font; - final name = selectedFont.fontFamilyDisplayName; - return MobileSettingItem( - name: LocaleKeys.settings_appearance_fontFamily_label.tr(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - lineHeight: 1.0, - name, - color: theme.colorScheme.onSurface, - ), - const Icon(Icons.chevron_right), - ], - ), - onTap: () async { - final newFont = await context.push(FontPickerScreen.routeName); - if (newFont != null && newFont != selectedFont) { - if (context.mounted) { - context.read().setFontFamily(newFont); - unawaited( - context.read().syncFontFamily(newFont), - ); - } - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language/language_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language/language_picker_screen.dart deleted file mode 100644 index 9b7d395b61..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language/language_picker_screen.dart +++ /dev/null @@ -1,52 +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/widgets.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/language.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class LanguagePickerScreen extends StatelessWidget { - const LanguagePickerScreen({super.key}); - - static const routeName = '/language_picker'; - - @override - Widget build(BuildContext context) => const LanguagePickerPage(); -} - -class LanguagePickerPage extends StatefulWidget { - const LanguagePickerPage({ - super.key, - }); - - @override - State createState() => _LanguagePickerPageState(); -} - -class _LanguagePickerPageState extends State { - @override - Widget build(BuildContext context) { - final supportedLocales = EasyLocalization.of(context)!.supportedLocales; - return Scaffold( - appBar: FlowyAppBar( - titleText: LocaleKeys.titleBar_language.tr(), - ), - body: SafeArea( - child: ListView.builder( - itemBuilder: (context, index) { - final locale = supportedLocales[index]; - return FlowyOptionTile.checkbox( - text: languageFromLocale(locale), - isSelected: EasyLocalization.of(context)!.locale == locale, - showTopBorder: false, - onTap: () => context.pop(locale), - backgroundColor: Colors.transparent, - ); - }, - itemCount: supportedLocales.length, - ), - ), - ); - } -} 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 deleted file mode 100644 index 6473485514..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/language.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 'setting.dart'; - -class LanguageSettingGroup extends StatefulWidget { - const LanguageSettingGroup({ - super.key, - }); - - @override - State createState() => _LanguageSettingGroupState(); -} - -class _LanguageSettingGroupState extends State { - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) { - return state.locale; - }, - builder: (context, locale) { - final theme = Theme.of(context); - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_menu_language.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_menu_language.tr(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - lineHeight: 1.0, - languageFromLocale(locale), - color: theme.colorScheme.onSurface, - ), - const Icon(Icons.chevron_right), - ], - ), - onTap: () async { - final newLocale = - await context.push(LanguagePickerScreen.routeName); - if (newLocale != null && newLocale != locale) { - if (context.mounted) { - context - .read() - .setLocale(context, newLocale); - } - } - }, - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/launch_settings_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/launch_settings_page.dart deleted file mode 100644 index 390b814d5c..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/launch_settings_page.dart +++ /dev/null @@ -1,39 +0,0 @@ -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/self_host_setting_group.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileLaunchSettingsPage extends StatelessWidget { - const MobileLaunchSettingsPage({ - super.key, - }); - - static const routeName = '/launch_settings'; - - @override - Widget build(BuildContext context) { - context.watch(); - return Scaffold( - appBar: FlowyAppBar( - titleText: LocaleKeys.settings_title.tr(), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - const LanguageSettingGroup(), - if (Env.enableCustomCloud) const SelfHostSettingGroup(), - const SupportSettingGroup(), - ], - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart deleted file mode 100644 index 8e77f362f6..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'widgets/widgets.dart'; - -class NotificationsSettingGroup extends StatefulWidget { - const NotificationsSettingGroup({super.key}); - - @override - State createState() => - _NotificationsSettingGroupState(); -} - -class _NotificationsSettingGroupState extends State { - bool isPushNotificationOn = false; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return MobileSettingGroup( - groupTitle: LocaleKeys.notificationHub_title.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_mobile_pushNotifications.tr(), - trailing: Switch.adaptive( - activeColor: theme.colorScheme.primary, - value: isPushNotificationOn, - onChanged: (bool value) { - setState(() { - isPushNotificationOn = value; - }); - }, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart deleted file mode 100644 index 04fe9e9cf7..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart +++ /dev/null @@ -1,77 +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'; -import 'package:go_router/go_router.dart'; - -class EditUsernameBottomSheet extends StatefulWidget { - const EditUsernameBottomSheet( - this.context, { - this.userName, - required this.onSubmitted, - super.key, - }); - final BuildContext context; - final String? userName; - final void Function(String) onSubmitted; - @override - State createState() => - _EditUsernameBottomSheetState(); -} - -class _EditUsernameBottomSheetState extends State { - late TextEditingController _textFieldController; - - final _formKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _textFieldController = TextEditingController(text: widget.userName); - } - - @override - void dispose() { - _textFieldController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - void submitUserName() { - if (_formKey.currentState!.validate()) { - final value = _textFieldController.text; - widget.onSubmitted.call(value); - widget.context.pop(); - } - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Form( - key: _formKey, - child: TextFormField( - controller: _textFieldController, - keyboardType: TextInputType.text, - validator: (value) { - if (value == null || value.isEmpty) { - return LocaleKeys.settings_mobile_usernameEmptyError.tr(); - } - return null; - }, - onEditingComplete: submitUserName, - ), - ), - const VSpace(16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: submitUserName, - child: Text(LocaleKeys.button_update.tr()), - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info.dart deleted file mode 100644 index 368a6d6f48..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'edit_username_bottom_sheet.dart'; -export 'personal_info_setting_group.dart'; 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 deleted file mode 100644 index 28ebdb750e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart +++ /dev/null @@ -1,73 +0,0 @@ -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'; -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 { - const PersonalInfoSettingGroup({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return BlocProvider( - create: (context) => getIt( - param1: userProfile, - )..add(const SettingsUserEvent.initial()), - child: BlocSelector( - selector: (state) => state.userProfile.name, - builder: (context, userName) { - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_accountPage_title.tr(), - settingItemList: [ - MobileSettingItem( - name: userName, - subtitle: isAuthEnabled && userProfile.email.isNotEmpty - ? Text( - userProfile.email, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface, - ), - ) - : null, - trailing: const Icon(Icons.chevron_right), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.settings_mobile_username.tr(), - showCloseButton: true, - showDragHandle: true, - showDivider: false, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (_) { - return EditUsernameBottomSheet( - context, - userName: userName, - onSubmitted: (value) => context - .read() - .add(SettingsUserEvent.updateUserName(name: 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 deleted file mode 100644 index dd19c2489d..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart'; -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(); -} - -class _SelfHostUrlBottomSheetState extends State { - final TextEditingController _textFieldController = TextEditingController(); - - final _formKey = GlobalKey(); - @override - void initState() { - super.initState(); - - _textFieldController.text = widget.url; - } - - @override - void dispose() { - _textFieldController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Form( - key: _formKey, - child: TextFormField( - controller: _textFieldController, - keyboardType: TextInputType.text, - validator: (value) { - if (value == null || - value.isEmpty || - validateUrl(value).isFailure) { - return LocaleKeys.settings_menu_invalidCloudURLScheme.tr(); - } - return null; - }, - onEditingComplete: _saveSelfHostUrl, - ), - ), - const SizedBox( - height: 16, - ), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _saveSelfHostUrl, - child: Text(LocaleKeys.settings_menu_restartApp.tr()), - ), - ), - ], - ); - } - - void _saveSelfHostUrl() { - if (_formKey.currentState!.validate()) { - final value = _textFieldController.text; - if (value.isNotEmpty) { - validateUrl(value).fold( - (url) async { - switch (widget.type) { - case SelfHostUrlBottomSheetType.shareDomain: - await useBaseWebDomain(url); - case SelfHostUrlBottomSheetType.cloudURL: - 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 deleted file mode 100644 index da2e0c773e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart +++ /dev/null @@ -1,123 +0,0 @@ -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'; -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'; - -class SelfHostSettingGroup extends StatefulWidget { - const SelfHostSettingGroup({ - super.key, - }); - - @override - State createState() => _SelfHostSettingGroupState(); -} - -class _SelfHostSettingGroupState extends State { - final future = Future.wait([ - getAppFlowyCloudUrl(), - getAppFlowyShareDomain(), - ]); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - final data = snapshot.data; - if (!snapshot.hasData || data == null || data.length != 2) { - return const SizedBox.shrink(); - } - final url = data[0]; - final shareDomain = data[1]; - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_menu_cloudAppFlowy.tr(), - settingItemList: [ - _buildSelfHostField(url), - _buildShareDomainField(shareDomain), - ], - ); - }, - ); - } - - Widget _buildSelfHostField(String url) { - return MobileSettingItem( - title: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText( - LocaleKeys.settings_menu_cloudURL.tr(), - fontSize: 12.0, - color: Theme.of(context).hintColor, - ), - ), - subtitle: FlowyText( - url, - ), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.editor_urlHint.tr(), - showCloseButton: true, - showDivider: false, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - builder: (_) { - return SelfHostUrlBottomSheet( - url: url, - type: SelfHostUrlBottomSheetType.cloudURL, - ); - }, - ); - }, - ); - } - - Widget _buildShareDomainField(String shareDomain) { - return MobileSettingItem( - title: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText( - LocaleKeys.settings_menu_webURL.tr(), - fontSize: 12.0, - color: Theme.of(context).hintColor, - ), - ), - subtitle: FlowyText( - shareDomain, - ), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.editor_urlHint.tr(), - showCloseButton: true, - showDivider: false, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - builder: (_) { - return SelfHostUrlBottomSheet( - url: shareDomain, - type: SelfHostUrlBottomSheetType.shareDomain, - ); - }, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart deleted file mode 100644 index 992e9be903..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'about/about.dart'; -export 'appearance/appearance_setting_group.dart'; -export 'font/font_setting.dart'; -export 'language_setting_group.dart'; -export 'notifications_setting_group.dart'; -export 'personal_info/personal_info.dart'; -export 'support_setting_group.dart'; -export 'widgets/widgets.dart'; 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 deleted file mode 100644 index e5e4efef77..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/core/helpers/url_launcher.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/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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -import 'widgets/widgets.dart'; - -class SupportSettingGroup extends StatelessWidget { - const SupportSettingGroup({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, snapshot) => MobileSettingGroup( - groupTitle: LocaleKeys.settings_mobile_support.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_mobile_joinDiscord.tr(), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () => afLaunchUrlString('https://discord.gg/JucBXeU2FE'), - ), - MobileSettingItem( - name: LocaleKeys.workspace_errorActions_reportIssue.tr(), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () { - showMobileBottomSheet( - context, - showDragHandle: true, - showHeader: true, - title: LocaleKeys.workspace_errorActions_reportIssue.tr(), - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (context) { - return _ReportIssuesWidget( - version: snapshot.data?.version ?? '', - ); - }, - ); - }, - ), - MobileSettingItem( - name: LocaleKeys.settings_files_clearCache.tr(), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () async { - await showFlowyMobileConfirmDialog( - context, - title: FlowyText( - LocaleKeys.settings_files_areYouSureToClearCache.tr(), - maxLines: 2, - ), - content: FlowyText( - LocaleKeys.settings_files_clearCacheDesc.tr(), - fontSize: 12, - maxLines: 4, - ), - 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(), - ); - } - }, - ); - }, - ), - ], - ), - ); - } -} - -class _ReportIssuesWidget extends StatelessWidget { - const _ReportIssuesWidget({ - required this.version, - }); - - final String version; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.workspace_errorActions_reportIssueOnGithub.tr(), - onTap: () { - final String os = Platform.operatingSystem; - afLaunchUrlString( - 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os', - ); - }, - ), - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.workspace_errorActions_exportLogFiles.tr(), - onTap: () => shareLogFiles(context), - ), - ], - ); - } -} 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 deleted file mode 100644 index 5ca5525099..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart +++ /dev/null @@ -1,227 +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/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'; -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(), - ), - ), - const VSpace(18.0), - _buildCheckbox(), - const VSpace(18.0), - MobileLogoutButton( - text: LocaleKeys.button_deleteAccount.tr(), - textColor: Theme.of(context).colorScheme.error, - onPressed: () => deleteMyAccount( - 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, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart deleted file mode 100644 index 17e9a62867..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileSettingGroup extends StatelessWidget { - const MobileSettingGroup({ - required this.groupTitle, - required this.settingItemList, - this.showDivider = true, - super.key, - }); - - final String groupTitle; - final List settingItemList; - final bool showDivider; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(4.0), - FlowyText.semibold( - groupTitle, - ), - const VSpace(4.0), - ...settingItemList, - showDivider ? const Divider() : const SizedBox.shrink(), - ], - ); - } -} 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 deleted file mode 100644 index 82c86065ae..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileSettingItem extends StatelessWidget { - const MobileSettingItem({ - super.key, - this.name, - this.padding = const EdgeInsets.only(bottom: 4), - this.trailing, - this.leadingIcon, - this.title, - this.subtitle, - this.onTap, - }); - - 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), - subtitle: subtitle, - trailing: trailing, - onTap: onTap, - visualDensity: VisualDensity.compact, - contentPadding: const EdgeInsets.only(left: 8.0), - ), - ); - } - - 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/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/widgets.dart deleted file mode 100644 index fb090cd3b4..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/widgets.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'mobile_setting_group_widget.dart'; -export 'mobile_setting_item_widget.dart'; 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 62aa114ef3..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.add) { - 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.invite) { - 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.remove) { - 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.inviteWorkspaceMember(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 501fd18ef7..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.removeWorkspaceMember( - 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_option_decorate_box.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart deleted file mode 100644 index 55ba32d06e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; - -class FlowyOptionDecorateBox extends StatelessWidget { - const FlowyOptionDecorateBox({ - super.key, - this.showTopBorder = true, - this.showBottomBorder = true, - this.color, - required this.child, - }); - - final bool showTopBorder; - final bool showBottomBorder; - final Widget child; - final Color? color; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: color ?? Theme.of(context).colorScheme.surface, - border: Border( - top: showTopBorder - ? BorderSide( - color: Theme.of(context).dividerColor, - width: 0.5, - ) - : BorderSide.none, - bottom: showBottomBorder - ? BorderSide( - color: Theme.of(context).dividerColor, - width: 0.5, - ) - : BorderSide.none, - ), - ), - child: child, - ); - } -} 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 deleted file mode 100644 index 191deb1e9f..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ /dev/null @@ -1,71 +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 MobileQuickActionButton extends StatelessWidget { - const MobileQuickActionButton({ - super.key, - required this.onTap, - required this.icon, - required this.text, - this.textColor, - this.iconColor, - this.iconSize, - this.enable = true, - this.rightIconBuilder, - }); - - final VoidCallback onTap; - final FlowySvgData icon; - 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, - child: InkWell( - onTap: enable ? onTap : null, - overlayColor: - enable ? null : const WidgetStatePropertyAll(Colors.transparent), - splashColor: Colors.transparent, - child: Container( - height: 52, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - FlowySvg( - icon, - size: iconSize, - color: iconColor, - ), - HSpace(30 - iconSize.width), - Expanded( - child: FlowyText.regular( - text, - fontSize: 16, - color: textColor, - ), - ), - if (rightIconBuilder != null) rightIconBuilder!(context), - ], - ), - ), - ), - ); - } -} - -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_mobile_search_text_field.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_search_text_field.dart deleted file mode 100644 index 9ec953d6b6..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_search_text_field.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class FlowyMobileSearchTextField extends StatelessWidget { - const FlowyMobileSearchTextField({ - super.key, - this.hintText, - this.controller, - this.onChanged, - this.onSubmitted, - }); - - final String? hintText; - final TextEditingController? controller; - final ValueChanged? onChanged; - final ValueChanged? onSubmitted; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 44.0, - child: CupertinoSearchTextField( - controller: controller, - onChanged: onChanged, - onSubmitted: onSubmitted, - placeholder: hintText, - prefixIcon: const FlowySvg(FlowySvgs.m_search_m), - prefixInsets: const EdgeInsets.only(left: 16.0, right: 2.0), - suffixIcon: const Icon(Icons.close), - suffixInsets: const EdgeInsets.only(right: 16.0), - placeholderStyle: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w400, - fontSize: 14.0, - ), - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).textTheme.bodyMedium?.color, - fontWeight: FontWeight.w400, - fontSize: 14.0, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart deleted file mode 100644 index 8aea36fbb9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -enum _FlowyMobileStateContainerType { - info, - error, -} - -/// Used to display info(like empty state) or error state -/// error state has two buttons to report issue with error message or reach out on discord -class FlowyMobileStateContainer extends StatelessWidget { - const FlowyMobileStateContainer.error({ - this.emoji, - required this.title, - this.description, - required this.errorMsg, - super.key, - }) : _stateType = _FlowyMobileStateContainerType.error; - - const FlowyMobileStateContainer.info({ - this.emoji, - required this.title, - this.description, - super.key, - }) : errorMsg = null, - _stateType = _FlowyMobileStateContainerType.info; - - final String? emoji; - final String title; - final String? description; - final String? errorMsg; - final _FlowyMobileStateContainerType _stateType; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return SizedBox.expand( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - emoji ?? - (_stateType == _FlowyMobileStateContainerType.error - ? '🛸' - : ''), - style: const TextStyle(fontSize: 40), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - title, - style: theme.textTheme.labelLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Text( - description ?? '', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.hintColor, - ), - textAlign: TextAlign.center, - ), - if (_stateType == _FlowyMobileStateContainerType.error) ...[ - const SizedBox(height: 8), - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, snapshot) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - OutlinedButton( - onPressed: () { - final String? version = snapshot.data?.version; - final String os = Platform.operatingSystem; - afLaunchUrlString( - 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os&context=Error%20log:%20$errorMsg', - ); - }, - child: Text( - LocaleKeys.workspace_errorActions_reportIssue.tr(), - ), - ), - OutlinedButton( - onPressed: () => - afLaunchUrlString('https://discord.gg/JucBXeU2FE'), - child: Text( - LocaleKeys.workspace_errorActions_reachOut.tr(), - ), - ), - ], - ); - }, - ), - ], - ], - ), - ), - ); - } -} 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 deleted file mode 100644 index f38c724a22..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ /dev/null @@ -1,328 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -enum FlowyOptionTileType { - text, - textField, - checkbox, - toggle, -} - -class FlowyOptionTile extends StatelessWidget { - const FlowyOptionTile._({ - super.key, - required this.type, - this.showTopBorder = true, - this.showBottomBorder = true, - this.text, - this.textColor, - this.controller, - this.leading, - this.onTap, - this.trailing, - this.textFieldPadding = const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 2.0, - ), - this.isSelected = false, - this.onValueChanged, - this.textFieldHintText, - this.onTextChanged, - this.onTextSubmitted, - this.autofocus, - this.content, - this.backgroundColor, - this.fontFamily, - this.height, - this.enable = true, - }); - - factory FlowyOptionTile.text({ - String? text, - Widget? content, - Color? textColor, - bool showTopBorder = true, - bool showBottomBorder = true, - Widget? leftIcon, - Widget? trailing, - VoidCallback? onTap, - double? height, - bool enable = true, - }) { - return FlowyOptionTile._( - type: FlowyOptionTileType.text, - text: text, - content: content, - textColor: textColor, - onTap: onTap, - showTopBorder: showTopBorder, - showBottomBorder: showBottomBorder, - leading: leftIcon, - trailing: trailing, - height: height, - enable: enable, - ); - } - - factory FlowyOptionTile.textField({ - required TextEditingController controller, - void Function(String value)? onTextChanged, - void Function(String value)? onTextSubmitted, - EdgeInsets textFieldPadding = const EdgeInsets.symmetric( - vertical: 16.0, - ), - bool showTopBorder = true, - bool showBottomBorder = true, - Widget? leftIcon, - Widget? trailing, - String? textFieldHintText, - bool autofocus = false, - bool enable = true, - }) { - return FlowyOptionTile._( - type: FlowyOptionTileType.textField, - controller: controller, - textFieldPadding: textFieldPadding, - showTopBorder: showTopBorder, - showBottomBorder: showBottomBorder, - leading: leftIcon, - trailing: trailing, - textFieldHintText: textFieldHintText, - onTextChanged: onTextChanged, - onTextSubmitted: onTextSubmitted, - autofocus: autofocus, - enable: enable, - ); - } - - factory FlowyOptionTile.checkbox({ - Key? key, - required String text, - required bool isSelected, - required VoidCallback? onTap, - Color? textColor, - Widget? leftIcon, - Widget? content, - bool showTopBorder = true, - bool showBottomBorder = true, - String? fontFamily, - Color? backgroundColor, - bool enable = true, - }) { - return FlowyOptionTile._( - key: key, - type: FlowyOptionTileType.checkbox, - isSelected: isSelected, - text: text, - textColor: textColor, - content: content, - onTap: onTap, - fontFamily: fontFamily, - backgroundColor: backgroundColor, - showTopBorder: showTopBorder, - showBottomBorder: showBottomBorder, - leading: leftIcon, - enable: enable, - trailing: isSelected - ? const FlowySvg( - FlowySvgs.m_blue_check_s, - blendMode: null, - ) - : null, - ); - } - - factory FlowyOptionTile.toggle({ - required String text, - required bool isSelected, - required void Function(bool value) onValueChanged, - void Function()? onTap, - bool showTopBorder = true, - bool showBottomBorder = true, - Widget? leftIcon, - bool enable = true, - }) { - return FlowyOptionTile._( - type: FlowyOptionTileType.toggle, - text: text, - onTap: onTap ?? () => onValueChanged(!isSelected), - onValueChanged: onValueChanged, - showTopBorder: showTopBorder, - showBottomBorder: showBottomBorder, - leading: leftIcon, - trailing: _Toggle(value: isSelected, onChanged: onValueChanged), - enable: enable, - ); - } - - final bool showTopBorder; - final bool showBottomBorder; - final String? text; - final Color? textColor; - final TextEditingController? controller; - final EdgeInsets textFieldPadding; - final void Function()? onTap; - final Widget? leading; - final Widget? trailing; - - // customize the content widget - final Widget? content; - - // only used in checkbox or switcher - final bool isSelected; - - // only used in switcher - final void Function(bool value)? onValueChanged; - - // only used in textfield - final String? textFieldHintText; - final void Function(String value)? onTextChanged; - final void Function(String value)? onTextSubmitted; - final bool? autofocus; - - final FlowyOptionTileType type; - - final Color? backgroundColor; - final String? fontFamily; - - final double? height; - - final bool enable; - - @override - Widget build(BuildContext context) { - final leadingWidget = _buildLeading(); - - Widget child = FlowyOptionDecorateBox( - color: backgroundColor, - showTopBorder: showTopBorder, - showBottomBorder: showBottomBorder, - child: SizedBox( - height: height, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - if (leadingWidget != null) leadingWidget, - if (content != null) content!, - if (content == null) _buildText(), - if (content == null) _buildTextField(), - if (trailing != null) trailing!, - ], - ), - ), - ), - ); - - if (type == FlowyOptionTileType.checkbox || - type == FlowyOptionTileType.toggle || - type == FlowyOptionTileType.text) { - child = GestureDetector( - onTap: onTap, - child: child, - ); - } - - if (!enable) { - child = Opacity( - opacity: 0.5, - child: IgnorePointer( - child: child, - ), - ); - } - - return child; - } - - Widget? _buildLeading() { - if (leading != null) { - return Center(child: leading); - } else { - return null; - } - } - - Widget _buildText() { - if (text == null || type == FlowyOptionTileType.textField) { - return const SizedBox.shrink(); - } - - final padding = EdgeInsets.symmetric( - horizontal: leading == null ? 0.0 : 12.0, - vertical: 14.0, - ); - - return Expanded( - child: Padding( - padding: padding, - child: FlowyText( - text!, - fontSize: 16, - color: textColor, - fontFamily: fontFamily, - ), - ), - ); - } - - Widget _buildTextField() { - if (controller == null) { - return const SizedBox.shrink(); - } - - return Expanded( - child: Container( - constraints: const BoxConstraints.tightFor( - height: 54.0, - ), - alignment: Alignment.center, - child: TextField( - controller: controller, - autofocus: autofocus ?? false, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: textFieldPadding, - hintText: textFieldHintText, - ), - onChanged: onTextChanged, - onSubmitted: onTextSubmitted, - ), - ), - ); - } -} - -class _Toggle extends StatelessWidget { - const _Toggle({ - required this.value, - required this.onChanged, - }); - - final bool value; - final void Function(bool value) onChanged; - - @override - Widget build(BuildContext context) { - // CupertinoSwitch adds a 8px margin all around. The original size of the - // switch is 38 x 22. - return SizedBox( - width: 46, - height: 30, - child: FittedBox( - fit: BoxFit.fill, - child: CupertinoSwitch( - value: value, - activeTrackColor: 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 deleted file mode 100644 index 96c18f5d91..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/tasks/app_widget.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'; - -enum ConfirmDialogActionAlignment { - // The action buttons are aligned vertically - // --------------------- - // | Action Button | - // | Cancel Button | - vertical, - // The action buttons are aligned horizontally - // --------------------- - // | Action Button | Cancel Button | - horizontal, -} - -/// show the dialog to confirm one single action -/// [onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog -Future showFlowyMobileConfirmDialog( - BuildContext context, { - Widget? title, - Widget? content, - ConfirmDialogActionAlignment actionAlignment = - ConfirmDialogActionAlignment.horizontal, - required String actionButtonTitle, - required VoidCallback? onActionButtonPressed, - Color? actionButtonColor, - String? cancelButtonTitle, - Color? cancelButtonColor, - VoidCallback? onCancelButtonPressed, -}) async { - return showDialog( - context: context, - builder: (dialogContext) { - final foregroundColor = Theme.of(context).colorScheme.onSurface; - final actionButton = TextButton( - child: FlowyText( - actionButtonTitle, - color: actionButtonColor ?? foregroundColor, - ), - onPressed: () { - onActionButtonPressed?.call(); - // we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog. - Navigator.of(dialogContext).pop(); - }, - ); - final cancelButton = TextButton( - child: FlowyText( - cancelButtonTitle ?? LocaleKeys.button_cancel.tr(), - color: cancelButtonColor ?? foregroundColor, - ), - onPressed: () { - onCancelButtonPressed?.call(); - Navigator.of(dialogContext).pop(); - }, - ); - - final actions = switch (actionAlignment) { - ConfirmDialogActionAlignment.horizontal => [ - actionButton, - cancelButton, - ], - ConfirmDialogActionAlignment.vertical => [ - Column( - children: [ - actionButton, - const Divider(height: 1, color: Colors.grey), - cancelButton, - ], - ), - ], - }; - - return AlertDialog.adaptive( - title: title, - content: content, - contentPadding: const EdgeInsets.symmetric( - horizontal: 24.0, - vertical: 4.0, - ), - actionsAlignment: MainAxisAlignment.center, - actions: actions, - ); - }, - ); -} - -Future showFlowyCupertinoConfirmDialog({ - BuildContext? context, - required String title, - Widget? content, - required Widget leftButton, - required Widget rightButton, - void Function(BuildContext context)? onLeftButtonPressed, - void Function(BuildContext context)? onRightButtonPressed, -}) { - return showDialog( - context: context ?? AppGlobals.context, - barrierColor: Colors.black.withValues(alpha: 0.25), - builder: (context) => CupertinoAlertDialog( - title: FlowyText.medium( - title, - fontSize: 16, - maxLines: 10, - figmaLineHeight: 22.0, - ), - content: content, - actions: [ - CupertinoDialogAction( - onPressed: () { - if (onLeftButtonPressed != null) { - onLeftButtonPressed(context); - } else { - Navigator.of(context).pop(); - } - }, - child: leftButton, - ), - CupertinoDialogAction( - onPressed: () { - if (onRightButtonPressed != null) { - onRightButtonPressed(context); - } else { - Navigator.of(context).pop(); - } - }, - child: rightButton, - ), - ], - ), - ); -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart deleted file mode 100644 index 4b1c1eedef..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'flowy_mobile_option_decorate_box.dart'; -export 'flowy_mobile_state_container.dart'; -export 'flowy_option_tile.dart'; -export 'show_flowy_mobile_confirm_dialog.dart'; 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 deleted file mode 100644 index 47c1668a2c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart +++ /dev/null @@ -1,212 +0,0 @@ -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/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/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(); - } - - final String chatId; - final Int64? questionId; - - 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; - } - 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<_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()), - ); - } - } - - 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); - } - } -} - -@freezed -class ChatAIMessageEvent with _$ChatAIMessageEvent { - const factory ChatAIMessageEvent.updateText(String text) = _UpdateText; - 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, - required String text, - required MessageState messageState, - required List sources, - required AIChatProgress? progress, - }) = _ChatAIMessageState; - - factory ChatAIMessageState.initial( - dynamic text, - MetadataCollection metadata, - ) { - return ChatAIMessageState( - text: text is String ? text : "", - stream: text is AnswerStream ? text : null, - messageState: const MessageState.ready(), - sources: metadata.sources, - progress: metadata.progress, - ); - } -} - -@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 deleted file mode 100644 index 602b46f97a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ /dev/null @@ -1,690 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -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-error/code.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.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'; - -class ChatBloc extends Bloc { - ChatBloc({ - required this.chatId, - required this.userId, - }) : chatController = InMemoryChatController(), - listener = ChatMessageListener(chatId: chatId), - selectedSourcesNotifier = ValueNotifier([]), - super(ChatState.initial()) { - _startListening(); - _dispatch(); - _loadMessages(); - _loadSetting(); - } - - final String chatId; - final String userId; - final ChatMessageListener listener; - final ValueNotifier> selectedSourcesNotifier; - final ChatController chatController; - - /// The last streaming message id - String answerStreamMessageId = ''; - String questionStreamMessageId = ''; - - ChatMessagePB? lastSentMessage; - - /// Using a temporary map to associate the real message ID with the last streaming message ID. - /// - /// When a message is streaming, it does not have a real message ID. To maintain the relationship - /// between the real message ID and the last streaming message ID, we use this map to store the associations. - /// - /// This map will be updated when receiving a message from the server and its author type - /// 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(); - await listener.stop(); - final request = ViewIdPB(value: chatId); - unawaited(FolderEventCloseView(request).send()); - selectedSourcesNotifier.dispose(); - return super.close(); - } - - void _dispatch() { - 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; - } - }, - loadPreviousMessages: () { - if (isLoadingPreviousMessages) { - return; - } - - final oldestMessage = _getOldestMessage(); - - 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); - } - }, - didLoadPreviousMessages: (messages, hasMore) { - Log.debug("did load previous messages: ${messages.length}"); - - 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; - - emit( - state.copyWith( - promptResponseState: PromptResponseState.sendingQuestion, - ), - ); - }, - finishSending: () { - emit( - state.copyWith( - promptResponseState: PromptResponseState.streamingAnswer, - ), - ); - }, - stopStream: () async { - if (answerStream == null) { - return; - } - - // tell backend to stop - final payload = StopStreamPB(chatId: chatId); - await AIEventStopStream(payload).send(); - - // allow user input - emit( - state.copyWith( - promptResponseState: PromptResponseState.ready, - ), - ); - - // no need to remove old message if stream has started already - if (answerStream!.hasStarted) { - return; - } - - // 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); - }, - ); - }, - ); - } - - void _startListening() { - listener.start( - chatMessageCallback: (pb) { - if (isClosed) { - return; - } - - // 3 mean message response from AI - if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) { - temporaryMessageIDMap.putIfAbsent( - pb.messageId.toString(), - () => answerStreamMessageId, - ); - answerStreamMessageId = ''; - } - - // 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()); - } - }, - latestMessageCallback: (list) { - if (!isClosed) { - final messages = list.messages.map(_createTextMessage).toList(); - add(ChatEvent.didLoadLatestMessages(messages)); - } - }, - prevMessageCallback: (list) { - if (!isClosed) { - final messages = list.messages.map(_createTextMessage).toList(); - 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) { - if (!isClosed) { - add(ChatEvent.didReceiveChatSettings(settings: settings)); - } - }, - 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"), - ); - } - - bool _isOneTimeMessage(Message message) { - return message.metadata != null && - message.metadata!.containsKey(onetimeShotType); - } - - /// get the last message that is not a one-time message - Message? _getOldestMessage() { - return chatController.messages - .firstWhereOrNull((message) => !_isOneTimeMessage(message)); - } - - void _loadPreviousMessages(Int64? beforeMessageId) { - final payload = LoadPrevChatMessagePB( - chatId: chatId, - limit: Int64(10), - beforeMessageId: beforeMessageId, - ); - AIEventLoadPrevMessage(payload).send(); - } - - Future _startStreamingMessage( - String message, - PredefinedFormat? format, - Map? metadata, - ) 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(); - } - - // 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, - ); - - lastSentMessage = question; - add(const ChatEvent.finishSending()); - add(ChatEvent.receiveMessage(streamAnswer)); - } - }, - (err) { - if (!isClosed) { - Log.error("Failed to send message: ${err.msg}"); - - final metadata = { - onetimeShotType: OnetimeShotType.error, - if (err.code != ErrorCode.Internal) errorMessageTextKey: err.msg, - }; - - final error = TextMessage( - text: '', - metadata: metadata, - author: const User(id: systemUserId), - id: systemUserId, - createdAt: DateTime.now(), - ); - - add(const ChatEvent.failedSending()); - add(ChatEvent.receiveMessage(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"; - - return TextMessage( - id: answerStreamMessageId, - text: '', - author: User(id: "streamId:${nanoid()}"), - metadata: { - "$AnswerStream": stream, - messageQuestionIdKey: 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, - text: '', - ); - } - - Message _createTextMessage(ChatMessagePB message) { - String messageId = message.messageId.toString(); - - /// If the message id is in the temporary map, we will use the previous fake message id - if (temporaryMessageIDMap.containsKey(messageId)) { - messageId = temporaryMessageIDMap[messageId]!; - } - - return TextMessage( - author: User(id: message.authorId), - id: messageId, - text: message.content, - createdAt: message.createdAt.toDateTime(), - metadata: { - messageRefSourceJsonStringKey: message.metadata, - }, - ); - } - - 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.didLoadPreviousMessages( - List messages, - bool hasMore, - ) = _DidLoadPreviousMessages; - - // related questions - const factory ChatEvent.didReceiveRelatedQuestions( - List questions, - ) = _DidReceiveRelatedQueston; - - const factory ChatEvent.deleteMessage(Message message) = _DeleteMessage; -} - -@freezed -class ChatState with _$ChatState { - const factory ChatState({ - required LoadChatMessageStatus loadingState, - required PromptResponseState promptResponseState, - required bool clearErrorMessages, - }) = _ChatState; - - factory ChatState.initial() => const ChatState( - loadingState: LoadChatMessageStatus.loading, - promptResponseState: PromptResponseState.ready, - clearErrorMessages: false, - ); -} - -bool isOtherUserMessage(Message message) { - return message.author.id != aiResponseUserId && - message.author.id != systemUserId && - !message.author.id.startsWith("streamId:"); -} 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 8718255cd9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart +++ /dev/null @@ -1,74 +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 deleted file mode 100644 index 4667806286..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart +++ /dev/null @@ -1,81 +0,0 @@ -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-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'; - -import 'chat_notification.dart'; - -typedef ChatMessageCallback = void Function(ChatMessagePB message); -typedef ChatErrorMessageCallback = void Function(ChatMessageErrorPB message); -typedef LatestMessageCallback = void Function(ChatMessageListPB list); -typedef PrevMessageCallback = void Function(ChatMessageListPB list); - -class ChatMessageListener { - ChatMessageListener({required this.chatId}) { - _parser = ChatNotificationParser(id: chatId, callback: _callback); - _subscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - final String chatId; - StreamSubscription? _subscription; - ChatNotificationParser? _parser; - - ChatMessageCallback? chatMessageCallback; - ChatErrorMessageCallback? chatErrorMessageCallback; - LatestMessageCallback? latestMessageCallback; - PrevMessageCallback? prevMessageCallback; - void Function()? finishStreamingCallback; - - void start({ - ChatMessageCallback? chatMessageCallback, - ChatErrorMessageCallback? chatErrorMessageCallback, - LatestMessageCallback? latestMessageCallback, - PrevMessageCallback? prevMessageCallback, - void Function()? finishStreamingCallback, - }) { - this.chatMessageCallback = chatMessageCallback; - this.chatErrorMessageCallback = chatErrorMessageCallback; - this.latestMessageCallback = latestMessageCallback; - this.prevMessageCallback = prevMessageCallback; - this.finishStreamingCallback = finishStreamingCallback; - } - - void _callback( - ChatNotification ty, - FlowyResult result, - ) { - result.map((r) { - switch (ty) { - case ChatNotification.DidReceiveChatMessage: - chatMessageCallback?.call(ChatMessagePB.fromBuffer(r)); - break; - case ChatNotification.StreamChatMessageError: - chatErrorMessageCallback?.call(ChatMessageErrorPB.fromBuffer(r)); - break; - case ChatNotification.DidLoadLatestChatMessage: - latestMessageCallback?.call(ChatMessageListPB.fromBuffer(r)); - break; - case ChatNotification.DidLoadPrevChatMessage: - prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r)); - break; - case ChatNotification.FinishStreaming: - finishStreamingCallback?.call(); - break; - default: - break; - } - }); - } - - Future stop() async { - await _subscription?.cancel(); - _subscription = null; - } -} 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 deleted file mode 100644 index 7dc1b550c3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_notification.dart +++ /dev/null @@ -1,45 +0,0 @@ -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-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -class ChatNotificationParser - extends NotificationParser { - ChatNotificationParser({ - super.id, - required super.callback, - }) : super( - tyParser: (ty, source) => - source == "Chat" ? ChatNotification.valueOf(ty) : null, - errorParser: (bytes) => FlowyError.fromBuffer(bytes), - ); -} - -typedef ChatNotificationHandler = Function( - ChatNotification ty, - FlowyResult result, -); - -class ChatNotificationListener { - ChatNotificationListener({ - required String objectId, - required ChatNotificationHandler handler, - }) : _parser = ChatNotificationParser(id: objectId, callback: handler) { - _subscription = - RustStreamReceiver.listen((observable) => _parser?.parse(observable)); - } - - ChatNotificationParser? _parser; - StreamSubscription? _subscription; - - Future stop() async { - _parser = null; - await _subscription?.cancel(); - _subscription = null; - } -} 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 deleted file mode 100644 index bcd3713550..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -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() { - on( - (event, emit) { - event.when( - updateText: (String text) { - emit(state.copyWith(text: text)); - }, - updateMessageId: (String messageId) { - emit(state.copyWith(messageId: messageId)); - }, - receiveError: (String error) {}, - updateQuestionState: (QuestionMessageState newState) { - emit(state.copyWith(messageState: newState)); - }, - ); - }, - ); - } - - 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; -} - -@freezed -class ChatUserMessageState with _$ChatUserMessageState { - const factory ChatUserMessageState({ - required String text, - required String? messageId, - required QuestionMessageState messageState, - }) = _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; -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart deleted file mode 100644 index 76aba27dc0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart +++ /dev/null @@ -1,205 +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_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'; - -class AIChatPluginBuilder extends PluginBuilder { - @override - Plugin build(dynamic data) { - if (data is ViewPB) { - return AIChatPagePlugin(view: data); - } - - throw FlowyPluginException.invalidData; - } - - @override - String get menuName => "AI Chat"; - - @override - FlowySvgData get icon => FlowySvgs.chat_ai_page_s; - - @override - PluginType get pluginType => PluginType.chat; - - @override - ViewLayoutPB get layoutType => ViewLayoutPB.Chat; -} - -class AIChatPluginConfig implements PluginConfig { - @override - bool get creatable => true; -} - -class AIChatPagePlugin extends Plugin { - AIChatPagePlugin({ - required ViewPB view, - }) : 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, - notifier: notifier, - ); - - @override - PluginId get id => notifier.view.id; - - @override - PluginType get pluginType => PluginType.chat; - - @override - void init() { - _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.notifier, - }); - - final ViewInfoBloc viewInfoBloc; - final ChatSelectMessageBloc chatMessageSelectorBloc; - 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); - - @override - Widget buildWidget({ - required PluginContext context, - required bool shrinkWrap, - Map? data, - }) { - notifier.isDeleted.addListener(_onDeleted); - - 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), - ], - child: AIChatPage( - userProfile: context.userProfile!, - key: ValueKey(notifier.view.id), - view: notifier.view, - onDeleted: () => - context.onDeleted?.call(notifier.view, deletedViewIndex), - ), - ); - } - - 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 deleted file mode 100644 index 90085354db..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ /dev/null @@ -1,491 +0,0 @@ -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_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: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 '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_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 { - const AIChatPage({ - super.key, - required this.view, - required this.onDeleted, - required this.userProfile, - }); - - final ViewPB view; - final VoidCallback onDeleted; - 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, - ), - ), - ); - }, - ), - ); - } -} - -class _ChatContentPage extends StatelessWidget { - const _ChatContentPage({ - required this.view, - required this.userProfile, - }); - - final UserProfilePB userProfile; - final ViewPB view; - - @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, - ), - ), - ), - ), - ), - ), - ), - _wrapConstraints( - _Input(view: view), - ), - ], - ), - _ => const Center(child: CircularProgressIndicator.adaptive()), - }; - }, - ); - } - - Widget _wrapConstraints(Widget child) { - return Container( - constraints: const BoxConstraints(maxWidth: 784), - margin: UniversalPlatform.isDesktop - ? const EdgeInsets.symmetric(horizontal: 60.0) - : null, - child: child, - ); - } - - Widget _buildTextMessage( - BuildContext context, - TextMessage message, - ) { - final messageType = onetimeMessageTypeFromMeta( - message.metadata, - ); - - if (messageType == OnetimeShotType.error) { - return ChatErrorMessageWidget( - errorMessage: message.metadata?[errorMessageTextKey] ?? "", - ); - } - - 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, - ), - ); - }, - ); - } - - 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( - message: message, - animation: animation, - padding: const EdgeInsets.symmetric(vertical: 12.0), - receivedMessageScaleAnimationAlignment: Alignment.center, - 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, - ), - ); - }, - ); - } - - 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()); - }, - ); - }, - ); - } - - 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, - ), - ); - }, - ); - }, - ), - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 59b7fbd39b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart +++ /dev/null @@ -1,135 +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: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_ui/style_widget/text.dart'; -import 'package:string_validator/string_validator.dart'; - -import 'layout_define.dart'; - -class ChatAIAvatar extends StatelessWidget { - const ChatAIAvatar({super.key}); - - @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, - ), - ), - ); - } -} - -class ChatUserAvatar extends StatelessWidget { - const ChatUserAvatar({ - super.key, - required this.iconUrl, - required this.name, - this.defaultName, - }); - - final String iconUrl; - final String name; - final String? defaultName; - - @override - Widget build(BuildContext context) { - late final Widget child; - if (iconUrl.isEmpty) { - child = _buildEmptyAvatar(context); - } else if (isURL(iconUrl)) { - child = _buildUrlAvatar(context); - } else { - child = _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 Color color = ColorGenerator(name).toColor(); - const initialsCount = 2; - - // Taking the first letters of the name components and limiting to 2 elements - final nameInitials = nameOrDefault - .split(' ') - .where((element) => element.isNotEmpty) - .take(initialsCount) - .map((element) => element[0].toUpperCase()) - .join(); - - return ColoredBox( - color: color, - child: Center( - 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), - ), - ); - } - - 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 the 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; -} 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/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_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_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart deleted file mode 100644 index 2c09e77050..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ /dev/null @@ -1,91 +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/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.onQuestionSelected, - required this.relatedQuestions, - }); - - final void 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, - ), - ); - } else { - return Align( - alignment: AlignmentDirectional.centerStart, - child: RelatedQuestionItem( - question: relatedQuestions[index - 1], - onQuestionSelected: onQuestionSelected, - ), - ); - } - }, - ), - ); - } -} - -class RelatedQuestionItem extends StatelessWidget { - const RelatedQuestionItem({ - required this.question, - required this.onQuestionSelected, - super.key, - }); - - final String question; - final Function(String) onQuestionSelected; - - @override - Widget build(BuildContext context) { - return FlowyButton( - mainAxisAlignment: MainAxisAlignment.start, - text: Flexible( - child: FlowyText( - question, - lineHeight: 1.4, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - 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_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart deleted file mode 100644 index 30dc918f70..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.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/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:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class ChatWelcomePage extends StatelessWidget { - const ChatWelcomePage({ - required this.userProfile, - required this.onSelectedQuestion, - super.key, - }); - - final void Function(String) onSelectedQuestion; - final UserProfilePB userProfile; - - static final List desktopItems = [ - 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, - ), - ), - ); - } - - 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({ - required this.question, - required this.onSelected, - super.key, - }); - - final void Function(String) onSelected; - final String question; - - @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), - ), - 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: 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 deleted file mode 100644 index 380767105f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ /dev/null @@ -1,173 +0,0 @@ -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: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_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; - -import '../layout_define.dart'; -import 'ai_markdown_text.dart'; -import 'ai_message_bubble.dart'; -import 'ai_metadata.dart'; -import 'error_text_message.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({ - super.key, - required this.user, - required this.messageUserId, - required this.message, - required this.stream, - 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 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, - chatId: chatId, - questionId: questionId, - ), - child: BlocBuilder( - builder: (context, state) { - final loadingText = - state.progress?.step ?? LocaleKeys.chat_generatingResponse.tr(); - - 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(); - - return ChatErrorMessageWidget( - errorMessage: LocaleKeys - .settings_aiPage_keys_localAIInitializing - .tr(), - ); - }, - ), - ), - ); - }, - ), - ); - } -} 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/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 deleted file mode 100644 index c73100b59d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart +++ /dev/null @@ -1,86 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; - -import 'user_message_bubble.dart'; - -class ChatUserMessageWidget extends StatelessWidget { - const ChatUserMessageWidget({ - super.key, - required this.user, - required this.message, - }); - - final User user; - final TextMessage message; - - @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, - ), - ); - }, - ), - ), - ); - } - - 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 to reuse the markdown capabilities, e.g., for previews. -class TextMessageText extends StatelessWidget { - const TextMessageText({ - super.key, - required this.text, - }); - - /// Text that is shown as markdown. - final String text; - - @override - Widget build(BuildContext context) { - return FlowyText( - text, - lineHeight: 1.4, - maxLines: null, - 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/base/color/color_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/color/color_picker.dart deleted file mode 100644 index 635979debe..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/color/color_picker.dart +++ /dev/null @@ -1,83 +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'; - -class FlowyMobileColorPicker extends StatelessWidget { - const FlowyMobileColorPicker({ - super.key, - required this.onSelectedColor, - }); - - final void Function(FlowyColorOption? option) onSelectedColor; - - @override - Widget build(BuildContext context) { - const defaultColor = 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 ListView.separated( - itemBuilder: (context, index) { - final color = colors[index]; - return SizedBox( - height: 56, - child: FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - color.i18n, - ), - leftIcon: _ColorIcon( - color: color.color, - ), - leftIconSize: const Size.square(36.0), - iconPadding: 12.0, - margin: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 16.0, - ), - onTap: () => onSelectedColor(color), - ), - ); - }, - separatorBuilder: (_, __) => const Divider( - height: 1, - ), - itemCount: colors.length, - ); - } -} - -class _ColorIcon extends StatelessWidget { - const _ColorIcon({required this.color}); - - final Color color; - - @override - Widget build(BuildContext context) { - return SizedBox.square( - dimension: 24, - child: DecoratedBox( - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/base/color/color_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/color/color_picker_screen.dart deleted file mode 100644 index 1e7ce9c64e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/color/color_picker_screen.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; -import 'package:appflowy/plugins/base/color/color_picker.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class MobileColorPickerScreen extends StatelessWidget { - const MobileColorPickerScreen({super.key, this.title}); - - final String? title; - - static const routeName = '/color_picker'; - static const pageTitle = 'title'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: FlowyAppBar( - titleText: title ?? LocaleKeys.titleBar_pageIcon.tr(), - ), - body: SafeArea( - child: FlowyMobileColorPicker( - onSelectedColor: (option) => context.pop(option), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart b/frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart deleted file mode 100644 index 420d79a84c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -class DragHandle extends StatelessWidget { - const DragHandle({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Container( - height: 4, - width: 40, - margin: const EdgeInsets.symmetric(vertical: 6), - decoration: BoxDecoration( - color: Colors.grey.shade400, - borderRadius: BorderRadius.circular(2), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart deleted file mode 100644 index 27b288090a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ /dev/null @@ -1,154 +0,0 @@ -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:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; - -// use a global value to store the selected emoji to prevent reloading every time. -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 int emojiPerLine; - final bool ensureFocus; - - @override - State createState() => _FlowyEmojiPickerState(); -} - -class _FlowyEmojiPickerState extends State { - late EmojiData emojiData; - bool loaded = false; - - @override - void initState() { - super.initState(); - - // load the emoji data from cache if it's available - if (kCachedEmojiData != null) { - loadEmojis(kCachedEmojiData!); - } else { - EmojiData.builtIn().then( - (value) { - kCachedEmojiData = value; - loadEmojis(value); - }, - ); - } - } - - @override - Widget build(BuildContext context) { - if (!loaded) { - return const Center( - child: SizedBox.square( - dimension: 24.0, - child: CircularProgressIndicator( - strokeWidth: 2.0, - ), - ), - ); - } - - return EmojiPicker( - emojiData: emojiData, - configuration: EmojiPickerConfiguration( - showTabs: false, - defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, - ), - onEmojiSelected: (id, emoji) { - widget.onEmojiSelected.call( - EmojiPickerResult(emojiId: id, emoji: emoji), - ); - 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, - child: FlowyButton( - margin: EdgeInsets.zero, - radius: Corners.s8Border, - text: FlowyTooltip( - message: name, - preferBelow: false, - child: 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); - }, - ), - ); - }, - ); - } - - 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 deleted file mode 100644 index 9b41dd8bce..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.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({ - super.key, - required this.category, - }); - - final Category category; - - @override - Widget build(BuildContext context) { - if (UniversalPlatform.isDesktop) { - return Container( - height: 22, - color: Theme.of(context).cardColor, - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText.regular( - category.id.capitalize(), - color: Theme.of(context).hintColor, - ), - ), - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 40, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - color: Theme.of(context).cardColor, - child: Padding( - padding: const EdgeInsets.only( - top: 14.0, - bottom: 4.0, - ), - child: FlowyText.regular(category.id), - ), - ), - const Divider( - height: 1, - thickness: 1, - ), - ], - ); - } - } -} 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 deleted file mode 100644 index 47f257d174..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:easy_localization/easy_localization.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], - }); - - 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); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart deleted file mode 100644 index 9df541f4a2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:io'; - -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -// used to prevent loading font from google fonts every time -List? _cachedFallbackFontFamily; - -// Some emojis are not supported by the default font on Android or Linux, fallback to noto color emoji -class EmojiText extends StatelessWidget { - const EmojiText({ - super.key, - required this.emoji, - required this.fontSize, - this.textAlign, - this.lineHeight, - }); - - final String emoji; - final double fontSize; - final TextAlign? textAlign; - final double? lineHeight; - - @override - Widget build(BuildContext context) { - _loadFallbackFontFamily(); - return FlowyText( - emoji, - fontSize: fontSize, - textAlign: textAlign, - strutStyle: const StrutStyle(forceStrutHeight: true), - fallbackFontFamily: _cachedFallbackFontFamily, - lineHeight: lineHeight, - isEmoji: true, - ); - } - - void _loadFallbackFontFamily() { - if (Platform.isLinux) { - final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily; - if (notoColorEmoji != null) { - _cachedFallbackFontFamily = [notoColorEmoji]; - } - } - } -} 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 b25bb5af06..afd1ef5545 100644 --- a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart +++ b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart @@ -1,11 +1,9 @@ -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 'package:appflowy/startup/plugin/plugin.dart'; class BlankPluginBuilder extends PluginBuilder { @override @@ -17,13 +15,10 @@ class BlankPluginBuilder extends PluginBuilder { String get menuName => "Blank"; @override - FlowySvgData get icon => const FlowySvgData(''); + String get menuIcon => ""; @override PluginType get pluginType => PluginType.blank; - - @override - ViewLayoutPB get layoutType => ViewLayoutPB.Document; } class BlankPluginConfig implements PluginConfig { @@ -44,29 +39,18 @@ 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; - - @override - Widget buildWidget({ - required PluginContext context, - required bool shrinkWrap, - Map? data, - }) => - const BlankPage(); + Widget buildWidget({PluginContext? context}) => const BlankPage(); @override List get navigationItems => [this]; } class BlankPage extends StatefulWidget { - const BlankPage({super.key}); + const BlankPage({Key? key}) : super(key: key); @override State createState() => _BlankPageState(); @@ -78,9 +62,9 @@ class _BlankPageState extends State { return SizedBox.expand( child: Container( color: Theme.of(context).colorScheme.surface, - child: const Padding( - padding: EdgeInsets.all(10), - child: SizedBox.shrink(), + child: Padding( + padding: const EdgeInsets.all(10), + child: Container(), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart deleted file mode 100644 index 707671d4d6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension CalcTypeLabel on CalculationType { - String get label => switch (this) { - CalculationType.Average => - LocaleKeys.grid_calculationTypeLabel_average.tr(), - CalculationType.Max => LocaleKeys.grid_calculationTypeLabel_max.tr(), - CalculationType.Median => - LocaleKeys.grid_calculationTypeLabel_median.tr(), - CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(), - CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(), - CalculationType.Count => - LocaleKeys.grid_calculationTypeLabel_count.tr(), - CalculationType.CountEmpty => - LocaleKeys.grid_calculationTypeLabel_countEmpty.tr(), - CalculationType.CountNonEmpty => - LocaleKeys.grid_calculationTypeLabel_countNonEmpty.tr(), - _ => throw UnimplementedError( - 'Label for $this has not been implemented', - ), - }; - - String get shortLabel => switch (this) { - CalculationType.CountEmpty => - LocaleKeys.grid_calculationTypeLabel_countEmptyShort.tr(), - CalculationType.CountNonEmpty => - LocaleKeys.grid_calculationTypeLabel_countNonEmptyShort.tr(), - _ => label, - }; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart deleted file mode 100644 index e074a9b283..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.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:flowy_infra/notifier.dart'; - -typedef UpdateCalculationValue - = FlowyResult; - -class CalculationsListener { - CalculationsListener({required this.viewId}); - - final String viewId; - - PublishNotifier? _calculationNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(UpdateCalculationValue) onCalculationChanged, - }) { - _calculationNotifier?.addPublishListener(onCalculationChanged); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateCalculation: - _calculationNotifier?.value = result.fold( - (payload) => FlowyResult.success( - CalculationChangesetNotificationPB.fromBuffer(payload), - ), - (err) => FlowyResult.failure(err), - ); - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _calculationNotifier?.dispose(); - _calculationNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart deleted file mode 100644 index e3ef8d578e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.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'; - -class CalculationsBackendService { - const CalculationsBackendService({required this.viewId}); - - final String viewId; - - // Get Calculations (initial fetch) - - Future> - getCalculations() async { - final payload = DatabaseViewIdPB()..value = viewId; - - return DatabaseEventGetAllCalculations(payload).send(); - } - - Future updateCalculation( - String fieldId, - CalculationType type, { - String? calculationId, - }) async { - final payload = UpdateCalculationChangesetPB() - ..viewId = viewId - ..fieldId = fieldId - ..calculationType = type; - - if (calculationId != null) { - payload.calculationId = calculationId; - } - - await DatabaseEventUpdateCalculation(payload).send(); - } - - Future removeCalculation( - String fieldId, - String calculationId, - ) async { - final payload = RemoveCalculationChangesetPB() - ..viewId = viewId - ..fieldId = fieldId - ..calculationId = calculationId; - - await DatabaseEventRemoveCalculation(payload).send(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart deleted file mode 100644 index 4b32a30b5d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart +++ /dev/null @@ -1,97 +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_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'checkbox_cell_bloc.freezed.dart'; - -class CheckboxCellBloc extends Bloc { - CheckboxCellBloc({ - required this.cellController, - }) : super(CheckboxCellState.initial(cellController)) { - _dispatch(); - } - - final CheckboxCellController cellController; - void Function()? _onCellChangedFn; - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener( - onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, - ); - } - await cellController.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) { - event.when( - initial: () => _startListening(), - didUpdateCell: (isSelected) { - emit(state.copyWith(isSelected: isSelected)); - }, - didUpdateField: (fieldName) { - emit(state.copyWith(fieldName: fieldName)); - }, - select: () { - cellController.saveCellData(state.isSelected ? "No" : "Yes"); - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellData) { - if (!isClosed) { - add(CheckboxCellEvent.didUpdateCell(_isSelected(cellData))); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(CheckboxCellEvent.didUpdateField(fieldInfo.name)); - } - } -} - -@freezed -class CheckboxCellEvent with _$CheckboxCellEvent { - const factory CheckboxCellEvent.initial() = _Initial; - const factory CheckboxCellEvent.select() = _Selected; - const factory CheckboxCellEvent.didUpdateCell(bool isSelected) = - _DidUpdateCell; - const factory CheckboxCellEvent.didUpdateField(String fieldName) = - _DidUpdateField; -} - -@freezed -class CheckboxCellState with _$CheckboxCellState { - const factory CheckboxCellState({ - required bool isSelected, - required String fieldName, - }) = _CheckboxCellState; - - factory CheckboxCellState.initial(CheckboxCellController cellController) { - return CheckboxCellState( - isSelected: _isSelected(cellController.getCellData()), - fieldName: cellController.fieldInfo.field.name, - ); - } -} - -bool _isSelected(CheckboxCellDataPB? cellData) { - return cellData != null && cellData.isChecked; -} 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 deleted file mode 100644 index 34c922aaf9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/domain/checklist_cell_service.dart'; -import 'package:appflowy_backend/log.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:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'checklist_cell_bloc.freezed.dart'; - -class ChecklistSelectOption { - ChecklistSelectOption({required this.isSelected, required this.data}); - - final bool isSelected; - final SelectOptionPB data; -} - -class ChecklistCellBloc extends Bloc { - ChecklistCellBloc({required this.cellController}) - : _checklistCellService = ChecklistCellBackendService( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - super(ChecklistCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final ChecklistCellController cellController; - final ChecklistCellBackendService _checklistCellService; - void Function()? _onCellChangedFn; - - int? nextPhantomIndex; - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(onCellChanged: _onCellChangedFn!); - } - await cellController.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didUpdateCell: (data) { - if (data == null) { - emit( - const ChecklistCellState( - tasks: [], - percent: 0, - showIncompleteOnly: false, - phantomIndex: null, - ), - ); - 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); - }, - selectTask: (id) async { - await _checklistCellService.select(optionId: id); - }, - createNewTask: (name, index) async { - await _createTask(name, index); - }, - 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, - ), - ); - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (data) { - if (!isClosed) { - add(ChecklistCellEvent.didUpdateCell(data)); - } - }, - ); - } - - 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); - result.fold((l) => null, (err) => Log.error(err)); - } - - Future _deleteOption(List options) async { - 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 -class ChecklistCellEvent with _$ChecklistCellEvent { - const factory ChecklistCellEvent.didUpdateCell( - ChecklistCellDataPB? data, - ) = _DidUpdateCell; - const factory ChecklistCellEvent.updateTaskName( - SelectOptionPB option, - String name, - ) = _UpdateTaskName; - const factory ChecklistCellEvent.selectTask(String taskId) = _SelectTask; - const factory ChecklistCellEvent.createNewTask( - String description, { - int? index, - }) = _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 -class ChecklistCellState with _$ChecklistCellState { - const factory ChecklistCellState({ - required List tasks, - required double percent, - required bool showIncompleteOnly, - required int? phantomIndex, - }) = _ChecklistCellState; - - factory ChecklistCellState.initial(ChecklistCellController cellController) { - final cellData = cellController.getCellData(loadIfNotExist: true); - - return ChecklistCellState( - tasks: _makeChecklistSelectOptions(cellData), - percent: cellData?.percentage ?? 0, - showIncompleteOnly: false, - phantomIndex: null, - ); - } -} - -List _makeChecklistSelectOptions( - ChecklistCellDataPB? data, -) { - if (data == null) { - return []; - } - return data.options - .map( - (option) => ChecklistSelectOption( - isSelected: data.selectedOptions.any( - (selected) => selected.id == option.id, - ), - data: option, - ), - ) - .toList(); -} 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 deleted file mode 100644 index 6f1d57fb50..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart +++ /dev/null @@ -1,101 +0,0 @@ -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'; -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 { - DateCellBloc({required this.cellController}) - : super(DateCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final DateCellController cellController; - VoidCallback? _onCellChangedFn; - - @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 { - event.when( - didReceiveCellUpdate: (DateCellDataPB? cellData) { - final dateCellData = DateCellData.fromPB(cellData); - emit( - state.copyWith( - cellData: dateCellData, - ), - ); - }, - didUpdateField: (fieldInfo) { - emit( - state.copyWith( - fieldInfo: fieldInfo, - ), - ); - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (data) { - if (!isClosed) { - add(DateCellEvent.didReceiveCellUpdate(data)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(DateCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class DateCellEvent with _$DateCellEvent { - const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = - _DidReceiveCellUpdate; - const factory DateCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; -} - -@freezed -class DateCellState with _$DateCellState { - const factory DateCellState({ - required FieldInfo fieldInfo, - required DateCellData cellData, - }) = _DateCellState; - - factory DateCellState.initial(DateCellController cellController) { - final cellData = DateCellData.fromPB(cellController.getCellData()); - - return DateCellState( - fieldInfo: cellController.fieldInfo, - cellData: cellData, - ); - } -} 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 deleted file mode 100644 index 8f0c37fb0d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart +++ /dev/null @@ -1,474 +0,0 @@ -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'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/user/application/reminder/reminder_extension.dart'; -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/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: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; - -part 'date_cell_editor_bloc.freezed.dart'; - -class DateCellEditorBloc - extends Bloc { - DateCellEditorBloc({ - required this.cellController, - required ReminderBloc reminderBloc, - }) : _reminderBloc = reminderBloc, - _dateCellBackendService = DateCellBackendService( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - super(DateCellEditorState.initial(cellController, reminderBloc)) { - _dispatch(); - _startListening(); - } - - final DateCellBackendService _dateCellBackendService; - final DateCellController cellController; - final ReminderBloc _reminderBloc; - - void Function()? _onCellChangedFn; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didReceiveCellUpdate: (DateCellDataPB? cellData) { - final dateCellData = DateCellData.fromPB(cellData); - - ReminderOption reminderOption = ReminderOption.none; - - if (dateCellData.reminderId.isNotEmpty && - 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(), - ); - } - } - - emit( - state.copyWith( - dateTime: dateCellData.dateTime, - endDateTime: dateCellData.endDateTime, - includeTime: dateCellData.includeTime, - isRange: dateCellData.isRange, - reminderId: dateCellData.reminderId, - reminderOption: reminderOption, - ), - ); - }, - didUpdateField: (field) { - final typeOption = DateTypeOptionDataParser() - .fromBuffer(field.field.typeOptionData); - emit(state.copyWith(dateTypeOptionPB: typeOption)); - }, - updateDateTime: (date) async { - if (state.isRange) { - return; - } - await _updateDateData(date: date); - }, - updateDateRange: (DateTime start, DateTime end) async { - if (!state.isRange) { - return; - } - await _updateDateData(date: start, endDate: end); - }, - setIncludeTime: (includeTime, dateTime, endDateTime) async { - await _updateIncludeTime(includeTime, dateTime, endDateTime); - }, - setIsRange: (isRange, dateTime, endDateTime) async { - await _updateIsRange(isRange, dateTime, endDateTime); - }, - 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) { - _reminderBloc - .add(ReminderEvent.remove(reminderId: state.reminderId)); - } - - await _clearDate(); - }, - setReminderOption: (ReminderOption option) async { - await _setReminderOption(option); - }, - ); - }, - ); - } - - Future> _updateDateData({ - DateTime? date, - DateTime? endDate, - bool updateReminderIfNecessary = true, - }) async { - final result = await _dateCellBackendService.update( - date: date, - endDate: endDate, - ); - 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); - } - - Future _updateIncludeTime( - bool includeTime, - DateTime? dateTime, - DateTime? endDateTime, - ) { - return _dateCellBackendService - .update( - date: dateTime, - endDate: endDateTime, - includeTime: includeTime, - ) - .fold((s) => _updateReminderIfNecessary(dateTime), Log.error); - } - - 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; - } - } - - // 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, - ); - result.onFailure(Log.error); - } - - void _updateReminderIfNecessary( - DateTime? dateTime, - ) { - if (state.reminderId.isEmpty || - state.reminderOption == ReminderOption.none || - dateTime == null) { - return; - } - - final scheduledAt = state.reminderOption.getNotificationDateTime(dateTime); - - // Update Reminder - _reminderBloc.add( - ReminderEvent.update( - ReminderUpdate( - id: state.reminderId, - scheduledAt: scheduledAt, - includeTime: state.includeTime, - ), - ), - ); - } - - String timeFormatPrompt(FlowyError error) { - return switch (state.dateTypeOptionPB.timeFormat) { - TimeFormatPB.TwelveHour => - "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 01:00 PM", - TimeFormatPB.TwentyFourHour => - "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 13:00", - _ => "", - }; - } - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener( - onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, - ); - } - return super.close(); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cell) { - if (!isClosed) { - add(DateCellEditorEvent.didReceiveCellUpdate(cell)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(DateCellEditorEvent.didUpdateField(fieldInfo)); - } - } - - Future _updateTypeOption( - Emitter emit, { - DateFormatPB? dateFormat, - TimeFormatPB? timeFormat, - }) async { - state.dateTypeOptionPB.freeze(); - final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) { - if (dateFormat != null) { - typeOption.dateFormat = dateFormat; - } - - if (timeFormat != null) { - typeOption.timeFormat = timeFormat; - } - }); - - final result = await FieldBackendService.updateFieldTypeOption( - viewId: cellController.viewId, - fieldId: cellController.fieldInfo.id, - typeOptionData: newDateTypeOption.writeToBuffer(), - ); - - result.onFailure(Log.error); - } -} - -@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.updateDateRange( - DateTime start, - DateTime end, - ) = _UpdateDateRange; - - const factory DateCellEditorEvent.setIncludeTime( - bool includeTime, - DateTime? dateTime, - DateTime? endDateTime, - ) = _IncludeTime; - - const factory DateCellEditorEvent.setIsRange( - bool isRange, - DateTime? dateTime, - DateTime? endDateTime, - ) = _SetIsRange; - - const factory DateCellEditorEvent.setReminderOption(ReminderOption option) = - _SetReminderOption; - - // date field type options are modified - const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) = - _SetTimeFormat; - - const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) = - _SetDateFormat; - - const factory DateCellEditorEvent.clearDate() = _ClearDate; -} - -@freezed -class DateCellEditorState with _$DateCellEditorState { - const factory DateCellEditorState({ - // the date field's type option - required DateTypeOptionPB dateTypeOptionPB, - - // cell data from the backend - required DateTime? dateTime, - required DateTime? endDateTime, - required bool includeTime, - required bool isRange, - required String reminderId, - @Default(ReminderOption.none) ReminderOption reminderOption, - }) = _DateCellEditorState; - - factory DateCellEditorState.initial( - DateCellController controller, - ReminderBloc reminderBloc, - ) { - final typeOption = controller.getTypeOption(DateTypeOptionDataParser()); - final cellData = controller.getCellData(); - final dateCellData = DateCellData.fromPB(cellData); - - ReminderOption reminderOption = ReminderOption.none; - - if (dateCellData.reminderId.isNotEmpty && dateCellData.dateTime != null) { - final reminder = reminderBloc.state.reminders - .firstWhereOrNull((r) => r.id == dateCellData.reminderId); - if (reminder != null) { - final eventDate = dateCellData.includeTime - ? dateCellData.dateTime! - : dateCellData.dateTime!.withoutTime; - reminderOption = ReminderOption.fromDateDifference( - eventDate, - reminder.scheduledAt.toDateTime(), - ); - } - } - - return DateCellEditorState( - dateTypeOptionPB: typeOption, - dateTime: dateCellData.dateTime, - endDateTime: dateCellData.endDateTime, - includeTime: dateCellData.includeTime, - isRange: dateCellData.isRange, - 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, - }); - - 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, - ); - } - - final DateTime? dateTime; - final DateTime? endDateTime; - final bool includeTime; - final bool isRange; - 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 deleted file mode 100644 index 73b2d2977b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart +++ /dev/null @@ -1,98 +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:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'number_cell_bloc.freezed.dart'; - -class NumberCellBloc extends Bloc { - NumberCellBloc({ - required this.cellController, - }) : super(NumberCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final NumberCellController cellController; - void Function()? _onCellChangedFn; - - @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( - didReceiveCellUpdate: (cellData) { - emit(state.copyWith(content: cellData ?? "")); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - updateCell: (text) async { - if (state.content != text) { - emit(state.copyWith(content: text)); - await cellController.saveCellData(text); - } - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellContent) { - if (!isClosed) { - add(NumberCellEvent.didReceiveCellUpdate(cellContent)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(NumberCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class NumberCellEvent with _$NumberCellEvent { - const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) = - _DidReceiveCellUpdate; - const factory NumberCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; - const factory NumberCellEvent.updateCell(String text) = _UpdateCell; -} - -@freezed -class NumberCellState with _$NumberCellState { - const factory NumberCellState({ - required String content, - required bool wrap, - }) = _NumberCellState; - - factory NumberCellState.initial(TextCellController cellController) { - final wrap = cellController.fieldInfo.wrapCellContent; - return NumberCellState( - content: cellController.getCellData() ?? "", - wrap: wrap ?? true, - ); - } -} 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 deleted file mode 100644 index ec789b03a0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart +++ /dev/null @@ -1,201 +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/relation_type_option_cubit.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy/plugins/database/domain/field_service.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-database2/protobuf.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'relation_cell_bloc.freezed.dart'; - -class RelationCellBloc extends Bloc { - RelationCellBloc({required this.cellController}) - : super(RelationCellState.initial(cellController)) { - _dispatch(); - _startListening(); - _init(); - } - - final RelationCellController cellController; - void Function()? _onCellChangedFn; - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener( - onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, - ); - } - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didUpdateCell: (cellData) async { - if (cellData == null || - cellData.rowIds.isEmpty || - state.relatedDatabaseMeta == null) { - emit(state.copyWith(rows: const [])); - return; - } - final payload = GetRelatedRowDataPB( - databaseId: state.relatedDatabaseMeta!.databaseId, - rowIds: cellData.rowIds, - ); - final result = - await DatabaseEventGetRelatedRowDatas(payload).send(); - final rows = result.fold( - (data) => data.rows, - (err) { - Log.error(err); - return const []; - }, - ); - emit(state.copyWith(rows: rows)); - }, - didUpdateField: (FieldInfo fieldInfo) async { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - final RelationTypeOptionPB typeOption = - cellController.getTypeOption(RelationTypeOptionDataParser()); - if (typeOption.databaseId.isEmpty) { - return; - } - final meta = await _loadDatabaseMeta(typeOption.databaseId); - emit(state.copyWith(relatedDatabaseMeta: meta)); - _loadCellData(); - }, - selectDatabaseId: (databaseId) async { - await _updateTypeOption(databaseId); - }, - selectRow: (rowId) async { - await _handleSelectRow(rowId); - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (data) { - if (!isClosed) { - add(RelationCellEvent.didUpdateCell(data)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(RelationCellEvent.didUpdateField(fieldInfo)); - } - } - - void _init() { - add(RelationCellEvent.didUpdateField(cellController.fieldInfo)); - } - - void _loadCellData() { - final cellData = cellController.getCellData(); - if (!isClosed && cellData != null) { - add(RelationCellEvent.didUpdateCell(cellData)); - } - } - - Future _handleSelectRow(String rowId) async { - final payload = RelationCellChangesetPB( - viewId: cellController.viewId, - cellId: CellIdPB( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - ); - if (state.rows.any((row) => row.rowId == rowId)) { - payload.removedRowIds.add(rowId); - } else { - payload.insertedRowIds.add(rowId); - } - final result = await DatabaseEventUpdateRelationCell(payload).send(); - result.fold((l) => null, (err) => Log.error(err)); - } - - Future _loadDatabaseMeta(String databaseId) async { - final getDatabaseResult = await DatabaseEventGetDatabases().send(); - final databaseMeta = getDatabaseResult.fold( - (s) => s.items.firstWhereOrNull( - (metaPB) => metaPB.databaseId == databaseId, - ), - (f) => null, - ); - if (databaseMeta != null) { - final result = await ViewBackendService.getView(databaseMeta.viewId); - return result.fold( - (s) => DatabaseMeta( - databaseId: databaseId, - viewId: databaseMeta.viewId, - databaseName: s.name, - ), - (f) => null, - ); - } - return null; - } - - Future _updateTypeOption(String databaseId) async { - final newDateTypeOption = RelationTypeOptionPB( - databaseId: databaseId, - ); - - final result = await FieldBackendService.updateFieldTypeOption( - viewId: cellController.viewId, - fieldId: cellController.fieldInfo.id, - typeOptionData: newDateTypeOption.writeToBuffer(), - ); - result.fold((s) => null, (err) => Log.error(err)); - } -} - -@freezed -class RelationCellEvent with _$RelationCellEvent { - const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) = - _DidUpdateCell; - const factory RelationCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; - const factory RelationCellEvent.selectDatabaseId( - String databaseId, - ) = _SelectDatabaseId; - const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId; -} - -@freezed -class RelationCellState with _$RelationCellState { - const factory RelationCellState({ - required DatabaseMeta? relatedDatabaseMeta, - required List rows, - required bool wrap, - }) = _RelationCellState; - - factory RelationCellState.initial(RelationCellController cellController) { - final wrap = cellController.fieldInfo.wrapCellContent; - return RelationCellState( - relatedDatabaseMeta: null, - rows: [], - wrap: wrap ?? true, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_row_search_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_row_search_bloc.dart deleted file mode 100644 index 2e07af6511..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_row_search_bloc.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.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:bloc/bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'relation_row_search_bloc.freezed.dart'; - -class RelationRowSearchBloc - extends Bloc { - RelationRowSearchBloc({ - required this.databaseId, - }) : super(RelationRowSearchState.initial()) { - _dispatch(); - _init(); - } - - final String databaseId; - final List allRows = []; - - void _dispatch() { - on( - (event, emit) { - event.when( - didUpdateRowList: (List rowList) { - allRows - ..clear() - ..addAll(rowList); - emit( - state.copyWith( - filteredRows: allRows, - focusedRowId: state.focusedRowId ?? allRows.firstOrNull?.rowId, - ), - ); - }, - updateFilter: (String filter) => _updateFilter(filter, emit), - updateFocusedOption: (String rowId) { - emit(state.copyWith(focusedRowId: rowId)); - }, - focusPreviousOption: () => _focusOption(true, emit), - focusNextOption: () => _focusOption(false, emit), - ); - }, - ); - } - - Future _init() async { - final payload = DatabaseIdPB(value: databaseId); - final result = await DatabaseEventGetRelatedDatabaseRows(payload).send(); - result.fold( - (data) => add(RelationRowSearchEvent.didUpdateRowList(data.rows)), - (err) => Log.error(err), - ); - } - - void _updateFilter(String filter, Emitter emit) { - final rows = [...allRows]; - - if (filter.isNotEmpty) { - rows.retainWhere( - (row) => - row.name.toLowerCase().contains(filter.toLowerCase()) || - (row.name.isEmpty && - LocaleKeys.grid_row_titlePlaceholder - .tr() - .toLowerCase() - .contains(filter.toLowerCase())), - ); - } - - final focusedRowId = rows.isEmpty - ? null - : rows.any((row) => row.rowId == state.focusedRowId) - ? state.focusedRowId - : rows.first.rowId; - - emit( - state.copyWith( - filteredRows: rows, - focusedRowId: focusedRowId, - ), - ); - } - - void _focusOption(bool previous, Emitter emit) { - if (state.filteredRows.isEmpty) { - return; - } - - final rowIds = state.filteredRows.map((e) => e.rowId).toList(); - final currentIndex = state.focusedRowId == null - ? -1 - : rowIds.indexWhere((id) => id == state.focusedRowId); - - // If the current index is -1, it means that the focused row is not in the list of row ids. - // In this case, we set the new index to the last index if previous is true, otherwise to 0. - final newIndex = currentIndex == -1 - ? (previous ? rowIds.length - 1 : 0) - : (currentIndex + (previous ? -1 : 1)) % rowIds.length; - - emit(state.copyWith(focusedRowId: rowIds[newIndex])); - } -} - -@freezed -class RelationRowSearchEvent with _$RelationRowSearchEvent { - const factory RelationRowSearchEvent.didUpdateRowList( - List rowList, - ) = _DidUpdateRowList; - const factory RelationRowSearchEvent.updateFilter(String filter) = - _UpdateFilter; - const factory RelationRowSearchEvent.updateFocusedOption( - String rowId, - ) = _UpdateFocusedOption; - const factory RelationRowSearchEvent.focusPreviousOption() = - _FocusPreviousOption; - const factory RelationRowSearchEvent.focusNextOption() = _FocusNextOption; -} - -@freezed -class RelationRowSearchState with _$RelationRowSearchState { - const factory RelationRowSearchState({ - required List filteredRows, - required String? focusedRowId, - }) = _RelationRowSearchState; - - factory RelationRowSearchState.initial() => const RelationRowSearchState( - filteredRows: [], - focusedRowId: null, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart deleted file mode 100644 index ca5af1e1a2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart +++ /dev/null @@ -1,105 +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_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'select_option_cell_bloc.freezed.dart'; - -class SelectOptionCellBloc - extends Bloc { - SelectOptionCellBloc({ - required this.cellController, - }) : super(SelectOptionCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final SelectOptionCellController cellController; - void Function()? _onCellChangedFn; - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener( - onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, - ); - } - await cellController.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) { - event.when( - didReceiveOptions: (List selectedOptions) { - emit( - state.copyWith( - selectedOptions: selectedOptions, - ), - ); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (selectOptionCellData) { - if (!isClosed) { - add( - SelectOptionCellEvent.didReceiveOptions( - selectOptionCellData?.selectOptions ?? [], - ), - ); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(SelectOptionCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class SelectOptionCellEvent with _$SelectOptionCellEvent { - const factory SelectOptionCellEvent.didReceiveOptions( - List selectedOptions, - ) = _DidReceiveOptions; - const factory SelectOptionCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; -} - -@freezed -class SelectOptionCellState with _$SelectOptionCellState { - const factory SelectOptionCellState({ - required List selectedOptions, - required bool wrap, - }) = _SelectOptionCellState; - - factory SelectOptionCellState.initial( - SelectOptionCellController cellController, - ) { - final data = cellController.getCellData(); - final wrap = cellController.fieldInfo.wrapCellContent; - return SelectOptionCellState( - selectedOptions: data?.selectOptions ?? [], - wrap: wrap ?? true, - ); - } -} 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 deleted file mode 100644 index c6e4e6484b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart +++ /dev/null @@ -1,458 +0,0 @@ -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'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy/plugins/database/domain/field_service.dart'; -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_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'select_option_cell_editor_bloc.freezed.dart'; - -const String createSelectOptionSuggestionId = - "create_select_option_suggestion_id"; - -class SelectOptionCellEditorBloc - extends Bloc { - SelectOptionCellEditorBloc({ - required this.cellController, - }) : _selectOptionService = SelectOptionCellBackendService( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - _typeOptionAction = cellController.fieldType == FieldType.SingleSelect - ? SingleSelectAction( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - onTypeOptionUpdated: (typeOptionData) => - FieldBackendService.updateFieldTypeOption( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - typeOptionData: typeOptionData, - ), - ) - : MultiSelectAction( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - onTypeOptionUpdated: (typeOptionData) => - FieldBackendService.updateFieldTypeOption( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - typeOptionData: typeOptionData, - ), - ), - super(SelectOptionCellEditorState.initial(cellController)) { - _dispatch(); - _startListening(); - final loadedOptions = _loadAllOptions(cellController); - add(SelectOptionCellEditorEvent.didUpdateOptions(loadedOptions)); - } - - final SelectOptionCellBackendService _selectOptionService; - final ISelectOptionAction _typeOptionAction; - final SelectOptionCellController cellController; - - VoidCallback? _onCellChangedFn; - - final List allOptions = []; - String filter = ""; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didUpdateCell: (selectedOptions) { - emit(state.copyWith(selectedOptions: selectedOptions)); - }, - didUpdateOptions: (options) { - allOptions - ..clear() - ..addAll(options); - final result = _getVisibleOptions(options); - emit( - state.copyWith( - options: result.options, - createSelectOptionSuggestion: - result.createSelectOptionSuggestion, - ), - ); - }, - createOption: () async { - if (state.createSelectOptionSuggestion == null) { - return; - } - filter = ""; - await _createOption( - name: state.createSelectOptionSuggestion!.name, - color: state.createSelectOptionSuggestion!.color, - ); - emit(state.copyWith(clearFilter: true)); - }, - deleteOption: (option) async { - await _deleteOption([option]); - }, - deleteAllOptions: () async { - if (allOptions.isNotEmpty) { - await _deleteOption(allOptions); - } - }, - updateOption: (option) async { - await _updateOption(option); - }, - selectOption: (optionId) async { - await _selectOptionService.select(optionIds: [optionId]); - }, - unselectOption: (optionId) async { - await _selectOptionService.unselect(optionIds: [optionId]); - }, - unselectLastOption: () async { - if (state.selectedOptions.isEmpty) { - return; - } - final lastSelectedOptionId = state.selectedOptions.last.id; - await _selectOptionService - .unselect(optionIds: [lastSelectedOptionId]); - }, - submitTextField: () { - _submitTextFieldValue(emit); - }, - selectMultipleOptions: (optionNames, remainder) { - if (optionNames.isNotEmpty) { - _selectMultipleOptions(optionNames); - } - _filterOption(remainder, emit); - }, - reorderOption: (fromOptionId, toOptionId) { - final options = _typeOptionAction.reorderOption( - allOptions, - fromOptionId, - toOptionId, - ); - allOptions - ..clear() - ..addAll(options); - final result = _getVisibleOptions(options); - emit(state.copyWith(options: result.options)); - }, - filterOption: (filterText) { - _filterOption(filterText, emit); - }, - focusPreviousOption: () { - _focusOption(true, emit); - }, - focusNextOption: () { - _focusOption(false, emit); - }, - updateFocusedOption: (optionId) { - emit(state.copyWith(focusedOptionId: optionId)); - }, - resetClearFilterFlag: () { - emit(state.copyWith(clearFilter: false)); - }, - ); - }, - ); - } - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener( - onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, - ); - } - return super.close(); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellData) { - if (!isClosed) { - add( - SelectOptionCellEditorEvent.didUpdateCell( - cellData == null ? [] : cellData.selectOptions, - ), - ); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - final loadedOptions = _loadAllOptions(cellController); - add(SelectOptionCellEditorEvent.didUpdateOptions(loadedOptions)); - } - } - - Future _createOption({ - required String name, - required SelectOptionColorPB color, - }) async { - final result = await _selectOptionService.create( - name: name, - color: color, - ); - result.fold((l) => {}, (err) => Log.error(err)); - } - - Future _deleteOption(List options) async { - final result = await _selectOptionService.delete(options: options); - result.fold((l) => null, (err) => Log.error(err)); - } - - Future _updateOption(SelectOptionPB option) async { - final result = await _selectOptionService.update( - option: option, - ); - - result.fold((l) => null, (err) => Log.error(err)); - } - - void _submitTextFieldValue(Emitter emit) { - if (state.focusedOptionId == null) { - return; - } - - final focusedOptionId = state.focusedOptionId!; - - if (focusedOptionId == createSelectOptionSuggestionId) { - filter = ""; - _createOption( - name: state.createSelectOptionSuggestion!.name, - color: state.createSelectOptionSuggestion!.color, - ); - emit( - state.copyWith( - createSelectOptionSuggestion: null, - clearFilter: true, - ), - ); - } else if (!state.selectedOptions - .any((option) => option.id == focusedOptionId)) { - _selectOptionService.select(optionIds: [focusedOptionId]); - emit( - state.copyWith( - clearFilter: true, - ), - ); - } - } - - void _selectMultipleOptions(List optionNames) { - final optionIds = optionNames - .map( - (name) => allOptions.firstWhereOrNull( - (option) => option.name.toLowerCase() == name.toLowerCase(), - ), - ) - .nonNulls - .map((option) => option.id) - .toList(); - - _selectOptionService.select(optionIds: optionIds); - } - - void _filterOption( - String filterText, - Emitter emit, - ) { - filter = filterText; - final _MakeOptionResult result = _getVisibleOptions( - allOptions, - ); - final focusedOptionId = result.options.isEmpty - ? result.createSelectOptionSuggestion == null - ? null - : createSelectOptionSuggestionId - : result.options.any((option) => option.id == state.focusedOptionId) - ? state.focusedOptionId - : result.options.first.id; - emit( - state.copyWith( - options: result.options, - createSelectOptionSuggestion: result.createSelectOptionSuggestion, - focusedOptionId: focusedOptionId, - ), - ); - } - - _MakeOptionResult _getVisibleOptions( - List allOptions, - ) { - final List options = List.from(allOptions); - String newOptionName = filter; - - if (filter.isNotEmpty) { - options.retainWhere((option) { - final name = option.name.toLowerCase(); - final lFilter = filter.toLowerCase(); - - if (name == lFilter) { - newOptionName = ""; - } - - return name.contains(lFilter); - }); - } - - return _MakeOptionResult( - options: options, - createSelectOptionSuggestion: newOptionName.isEmpty - ? null - : CreateSelectOptionSuggestion( - name: newOptionName, - color: newSelectOptionColor(allOptions), - ), - ); - } - - void _focusOption(bool previous, Emitter emit) { - if (state.options.isEmpty && state.createSelectOptionSuggestion == null) { - return; - } - - final optionIds = [ - ...state.options.map((e) => e.id), - if (state.createSelectOptionSuggestion != null) - createSelectOptionSuggestionId, - ]; - - if (state.focusedOptionId == null) { - emit( - state.copyWith( - focusedOptionId: previous ? optionIds.last : optionIds.first, - ), - ); - return; - } - - final currentIndex = - optionIds.indexWhere((id) => id == state.focusedOptionId); - - final newIndex = currentIndex == -1 - ? 0 - : (currentIndex + (previous ? -1 : 1)) % optionIds.length; - - emit(state.copyWith(focusedOptionId: optionIds[newIndex])); - } -} - -@freezed -class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent { - const factory SelectOptionCellEditorEvent.didUpdateCell( - List selectedOptions, - ) = _DidUpdateCell; - const factory SelectOptionCellEditorEvent.didUpdateOptions( - List options, - ) = _DidUpdateOptions; - 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.updateOption( - SelectOptionPB option, - ) = _UpdateOption; - const factory SelectOptionCellEditorEvent.deleteOption( - SelectOptionPB option, - ) = _DeleteOption; - const factory SelectOptionCellEditorEvent.deleteAllOptions() = - _DeleteAllOptions; - const factory SelectOptionCellEditorEvent.reorderOption( - String fromOptionId, - String toOptionId, - ) = _ReorderOption; - const factory SelectOptionCellEditorEvent.filterOption(String filterText) = - _SelectOptionFilter; - const factory SelectOptionCellEditorEvent.submitTextField() = - _SubmitTextField; - const factory SelectOptionCellEditorEvent.selectMultipleOptions( - List optionNames, - String remainder, - ) = _SelectMultipleOptions; - const factory SelectOptionCellEditorEvent.focusPreviousOption() = - _FocusPreviousOption; - const factory SelectOptionCellEditorEvent.focusNextOption() = - _FocusNextOption; - const factory SelectOptionCellEditorEvent.updateFocusedOption( - String? optionId, - ) = _UpdateFocusedOption; - const factory SelectOptionCellEditorEvent.resetClearFilterFlag() = - _ResetClearFilterFlag; -} - -@freezed -class SelectOptionCellEditorState with _$SelectOptionCellEditorState { - const factory SelectOptionCellEditorState({ - required List options, - required List selectedOptions, - required CreateSelectOptionSuggestion? createSelectOptionSuggestion, - required String? focusedOptionId, - required bool clearFilter, - }) = _SelectOptionEditorState; - - factory SelectOptionCellEditorState.initial( - SelectOptionCellController cellController, - ) { - final allOptions = _loadAllOptions(cellController); - final data = cellController.getCellData(); - return SelectOptionCellEditorState( - options: allOptions, - selectedOptions: data?.selectOptions ?? [], - createSelectOptionSuggestion: null, - focusedOptionId: null, - clearFilter: false, - ); - } -} - -class _MakeOptionResult { - _MakeOptionResult({ - required this.options, - required this.createSelectOptionSuggestion, - }); - - List options; - CreateSelectOptionSuggestion? createSelectOptionSuggestion; -} - -class CreateSelectOptionSuggestion { - CreateSelectOptionSuggestion({ - required this.name, - required this.color, - }); - - final String name; - final SelectOptionColorPB color; -} - -List _loadAllOptions( - SelectOptionCellController cellController, -) { - if (cellController.fieldType == FieldType.SingleSelect) { - return cellController - .getTypeOption( - SingleSelectTypeOptionDataParser(), - ) - .options; - } else { - return cellController - .getTypeOption( - MultiSelectTypeOptionDataParser(), - ) - .options; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_cell_bloc.dart deleted file mode 100644 index 34b3981f48..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_cell_bloc.dart +++ /dev/null @@ -1,111 +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:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'summary_cell_bloc.freezed.dart'; - -class SummaryCellBloc extends Bloc { - SummaryCellBloc({ - required this.cellController, - }) : super(SummaryCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final SummaryCellController cellController; - void Function()? _onCellChangedFn; - - @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( - didReceiveCellUpdate: (cellData) { - emit( - state.copyWith(content: cellData ?? ""), - ); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - updateCell: (text) async { - 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( - SummaryCellEvent.didReceiveCellUpdate( - cellController.getCellData() ?? "", - ), - ); - } - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellContent) { - if (!isClosed) { - add( - SummaryCellEvent.didReceiveCellUpdate(cellContent ?? ""), - ); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(SummaryCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class SummaryCellEvent with _$SummaryCellEvent { - const factory SummaryCellEvent.didReceiveCellUpdate(String? cellContent) = - _DidReceiveCellUpdate; - const factory SummaryCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; - const factory SummaryCellEvent.updateCell(String text) = _UpdateCell; -} - -@freezed -class SummaryCellState with _$SummaryCellState { - const factory SummaryCellState({ - required String content, - required bool wrap, - }) = _SummaryCellState; - - factory SummaryCellState.initial(SummaryCellController cellController) { - final wrap = cellController.fieldInfo.wrapCellContent; - return SummaryCellState( - content: cellController.getCellData() ?? "", - wrap: wrap ?? true, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_row_bloc.dart deleted file mode 100644 index fe69cdb364..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_row_bloc.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'summary_row_bloc.freezed.dart'; - -class SummaryRowBloc extends Bloc { - SummaryRowBloc({ - required this.viewId, - required this.rowId, - required this.fieldId, - }) : super(SummaryRowState.initial()) { - _dispatch(); - } - - final String viewId; - final String rowId; - final String fieldId; - - void _dispatch() { - on( - (event, emit) async { - event.when( - startSummary: () { - final params = SummaryRowPB( - viewId: viewId, - rowId: rowId, - fieldId: fieldId, - ); - emit( - state.copyWith( - loadingState: const LoadingState.loading(), - error: null, - ), - ); - - DatabaseEventSummarizeRow(params).send().then( - (result) => { - if (!isClosed) add(SummaryRowEvent.finishSummary(result)), - }, - ); - }, - finishSummary: (result) { - result.fold( - (s) => { - emit( - state.copyWith( - loadingState: const LoadingState.finish(), - error: null, - ), - ), - }, - (err) => { - emit( - state.copyWith( - loadingState: const LoadingState.finish(), - error: err, - ), - ), - }, - ); - }, - ); - }, - ); - } -} - -@freezed -class SummaryRowEvent with _$SummaryRowEvent { - const factory SummaryRowEvent.startSummary() = _DidStartSummary; - const factory SummaryRowEvent.finishSummary( - FlowyResult result, - ) = _DidFinishSummary; -} - -@freezed -class SummaryRowState with _$SummaryRowState { - const factory SummaryRowState({ - required LoadingState loadingState, - required FlowyError? error, - }) = _SummaryRowState; - - factory SummaryRowState.initial() { - return const SummaryRowState( - loadingState: LoadingState.finish(), - error: null, - ); - } -} - -@freezed -class LoadingState with _$LoadingState { - const factory LoadingState.loading() = _Loading; - const factory LoadingState.finish() = _Finish; -} 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 deleted file mode 100644 index 7960b34d7c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart +++ /dev/null @@ -1,117 +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:flutter/foundation.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'text_cell_bloc.freezed.dart'; - -class TextCellBloc extends Bloc { - TextCellBloc({required this.cellController}) - : super(TextCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final TextCellController cellController; - void Function()? _onCellChangedFn; - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener( - onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, - ); - } - await cellController.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) { - event.when( - didReceiveCellUpdate: (content) { - emit(state.copyWith(content: content)); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - 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) { - cellController.saveCellData(text, debounce: true); - } - }, - enableEdit: (bool enabled) { - emit(state.copyWith(enableEdit: enabled)); - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellContent) { - if (!isClosed) { - add(TextCellEvent.didReceiveCellUpdate(cellContent)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(TextCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class TextCellEvent with _$TextCellEvent { - 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; -} - -@freezed -class TextCellState with _$TextCellState { - const factory TextCellState({ - required String? content, - required ValueNotifier? emoji, - required ValueNotifier? hasDocument, - required bool enableEdit, - required bool wrap, - }) = _TextCellState; - - factory TextCellState.initial(TextCellController cellController) { - final cellData = cellController.getCellData(); - final wrap = cellController.fieldInfo.wrapCellContent ?? true; - ValueNotifier? emoji; - ValueNotifier? hasDocument; - if (cellController.fieldInfo.isPrimary) { - emoji = cellController.icon; - hasDocument = cellController.hasDocument; - } - - return TextCellState( - content: cellData, - emoji: emoji, - enableEdit: false, - hasDocument: hasDocument, - wrap: wrap, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart deleted file mode 100644 index 62ff95850f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart +++ /dev/null @@ -1,117 +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/util/time.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -part 'time_cell_bloc.freezed.dart'; - -class TimeCellBloc extends Bloc { - TimeCellBloc({ - required this.cellController, - }) : super(TimeCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final TimeCellController cellController; - void Function()? _onCellChangedFn; - - @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( - didReceiveCellUpdate: (content) { - emit( - state.copyWith( - content: - content != null ? formatTime(content.time.toInt()) : "", - ), - ); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - updateCell: (text) async { - text = parseTime(text)?.toString() ?? text; - 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( - TimeCellEvent.didReceiveCellUpdate( - cellController.getCellData(), - ), - ); - } - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellContent) { - if (!isClosed) { - add(TimeCellEvent.didReceiveCellUpdate(cellContent)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(TimeCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class TimeCellEvent with _$TimeCellEvent { - const factory TimeCellEvent.didReceiveCellUpdate(TimeCellDataPB? cell) = - _DidReceiveCellUpdate; - const factory TimeCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; - const factory TimeCellEvent.updateCell(String text) = _UpdateCell; -} - -@freezed -class TimeCellState with _$TimeCellState { - const factory TimeCellState({ - required String content, - required bool wrap, - }) = _TimeCellState; - - factory TimeCellState.initial(TimeCellController cellController) { - final wrap = cellController.fieldInfo.wrapCellContent; - final cellData = cellController.getCellData(); - return TimeCellState( - content: cellData != null ? formatTime(cellData.time.toInt()) : "", - wrap: wrap ?? true, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart deleted file mode 100644 index 6d80109c71..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart +++ /dev/null @@ -1,104 +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_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'timestamp_cell_bloc.freezed.dart'; - -class TimestampCellBloc extends Bloc { - TimestampCellBloc({ - required this.cellController, - }) : super(TimestampCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final TimestampCellController cellController; - void Function()? _onCellChangedFn; - - @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 { - event.when( - didReceiveCellUpdate: (TimestampCellDataPB? cellData) { - emit( - state.copyWith( - data: cellData, - dateStr: cellData?.dateTime ?? "", - ), - ); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (data) { - if (!isClosed) { - add(TimestampCellEvent.didReceiveCellUpdate(data)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(TimestampCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class TimestampCellEvent with _$TimestampCellEvent { - const factory TimestampCellEvent.didReceiveCellUpdate( - TimestampCellDataPB? data, - ) = _DidReceiveCellUpdate; - const factory TimestampCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; -} - -@freezed -class TimestampCellState with _$TimestampCellState { - const factory TimestampCellState({ - required TimestampCellDataPB? data, - required String dateStr, - required FieldInfo fieldInfo, - required bool wrap, - }) = _TimestampCellState; - - factory TimestampCellState.initial(TimestampCellController cellController) { - final cellData = cellController.getCellData(); - final wrap = cellController.fieldInfo.wrapCellContent; - - return TimestampCellState( - fieldInfo: cellController.fieldInfo, - data: cellData, - dateStr: cellData?.dateTime ?? "", - wrap: wrap ?? true, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_cell_bloc.dart deleted file mode 100644 index f31a4a1c91..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_cell_bloc.dart +++ /dev/null @@ -1,111 +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:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'translate_cell_bloc.freezed.dart'; - -class TranslateCellBloc extends Bloc { - TranslateCellBloc({ - required this.cellController, - }) : super(TranslateCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final TranslateCellController cellController; - void Function()? _onCellChangedFn; - - @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( - didReceiveCellUpdate: (cellData) { - emit( - state.copyWith(content: cellData ?? ""), - ); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - updateCell: (text) async { - 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( - TranslateCellEvent.didReceiveCellUpdate( - cellController.getCellData() ?? "", - ), - ); - } - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellContent) { - if (!isClosed) { - add( - TranslateCellEvent.didReceiveCellUpdate(cellContent ?? ""), - ); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(TranslateCellEvent.didUpdateField(fieldInfo)); - } - } -} - -@freezed -class TranslateCellEvent with _$TranslateCellEvent { - const factory TranslateCellEvent.didReceiveCellUpdate(String? cellContent) = - _DidReceiveCellUpdate; - const factory TranslateCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; - const factory TranslateCellEvent.updateCell(String text) = _UpdateCell; -} - -@freezed -class TranslateCellState with _$TranslateCellState { - const factory TranslateCellState({ - required String content, - required bool wrap, - }) = _TranslateCellState; - - factory TranslateCellState.initial(TranslateCellController cellController) { - final wrap = cellController.fieldInfo.wrapCellContent; - return TranslateCellState( - content: cellController.getCellData() ?? "", - wrap: wrap ?? true, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_row_bloc.dart deleted file mode 100644 index 4778df2c2a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_row_bloc.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'translate_row_bloc.freezed.dart'; - -class TranslateRowBloc extends Bloc { - TranslateRowBloc({ - required this.viewId, - required this.rowId, - required this.fieldId, - }) : super(TranslateRowState.initial()) { - _dispatch(); - } - - final String viewId; - final String rowId; - final String fieldId; - - void _dispatch() { - on( - (event, emit) async { - event.when( - startTranslate: () { - final params = TranslateRowPB( - viewId: viewId, - rowId: rowId, - fieldId: fieldId, - ); - emit( - state.copyWith( - loadingState: const LoadingState.loading(), - error: null, - ), - ); - - DatabaseEventTranslateRow(params).send().then( - (result) => { - if (!isClosed) - add(TranslateRowEvent.finishTranslate(result)), - }, - ); - }, - finishTranslate: (result) { - result.fold( - (s) => { - emit( - state.copyWith( - loadingState: const LoadingState.finish(), - error: null, - ), - ), - }, - (err) => { - emit( - state.copyWith( - loadingState: const LoadingState.finish(), - error: err, - ), - ), - }, - ); - }, - ); - }, - ); - } -} - -@freezed -class TranslateRowEvent with _$TranslateRowEvent { - const factory TranslateRowEvent.startTranslate() = _DidStartTranslate; - const factory TranslateRowEvent.finishTranslate( - FlowyResult result, - ) = _DidFinishTranslate; -} - -@freezed -class TranslateRowState with _$TranslateRowState { - const factory TranslateRowState({ - required LoadingState loadingState, - required FlowyError? error, - }) = _TranslateRowState; - - factory TranslateRowState.initial() { - return const TranslateRowState( - loadingState: LoadingState.finish(), - error: null, - ); - } -} - -@freezed -class LoadingState with _$LoadingState { - const factory LoadingState.loading() = _Loading; - const factory LoadingState.finish() = _Finish; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart deleted file mode 100644 index 81ea6d60d1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'url_cell_bloc.freezed.dart'; - -class URLCellBloc extends Bloc { - URLCellBloc({ - required this.cellController, - }) : super(URLCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - final URLCellController cellController; - void Function()? _onCellChangedFn; - - @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( - didUpdateCell: (cellData) async { - final content = cellData?.content ?? ""; - final isValid = await _isUrlValid(content); - emit( - state.copyWith( - content: content, - isValid: isValid, - ), - ); - }, - didUpdateField: (fieldInfo) { - final wrap = fieldInfo.wrapCellContent; - if (wrap != null) { - emit(state.copyWith(wrap: wrap)); - } - }, - updateURL: (String url) { - cellController.saveCellData(url, debounce: true); - }, - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellData) { - if (!isClosed) { - add(URLCellEvent.didUpdateCell(cellData)); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(URLCellEvent.didUpdateField(fieldInfo)); - } - } - - Future _isUrlValid(String content) async { - if (content.isEmpty) { - return true; - } - - try { - // check protocol is provided - const linkPrefix = [ - 'http://', - 'https://', - ]; - final shouldAddScheme = - !linkPrefix.any((pattern) => content.startsWith(pattern)); - final url = shouldAddScheme ? 'http://$content' : content; - - // get hostname and check validity - final uri = Uri.parse(url); - final hostName = uri.host; - await InternetAddress.lookup(hostName); - } catch (_) { - return false; - } - return true; - } -} - -@freezed -class URLCellEvent with _$URLCellEvent { - const factory URLCellEvent.updateURL(String url) = _UpdateURL; - const factory URLCellEvent.didUpdateCell(URLCellDataPB? cell) = - _DidUpdateCell; - const factory URLCellEvent.didUpdateField(FieldInfo fieldInfo) = - _DidUpdateField; -} - -@freezed -class URLCellState with _$URLCellState { - const factory URLCellState({ - required String content, - required bool isValid, - required bool wrap, - }) = _URLCellState; - - factory URLCellState.initial(URLCellController cellController) { - final cellData = cellController.getCellData(); - final wrap = cellController.fieldInfo.wrapCellContent; - return URLCellState( - content: cellData?.content ?? "", - isValid: true, - wrap: wrap ?? true, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart deleted file mode 100644 index 171f86f11d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:appflowy/plugins/database/application/row/row_service.dart'; - -import 'cell_controller.dart'; - -/// CellMemCache is used to cache cell data of each block. -/// We use CellContext to index the cell in the cache. -/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid -/// for more information -class CellMemCache { - CellMemCache(); - - /// fieldId: {rowId: cellData} - final Map> _cellByFieldId = {}; - - void removeCellWithFieldId(String fieldId) { - _cellByFieldId.remove(fieldId); - } - - void remove(CellContext context) { - _cellByFieldId[context.fieldId]?.remove(context.rowId); - } - - void insert(CellContext context, T data) { - _cellByFieldId.putIfAbsent(context.fieldId, () => {}); - _cellByFieldId[context.fieldId]![context.rowId] = data; - } - - T? get(CellContext context) { - final value = _cellByFieldId[context.fieldId]?[context.rowId]; - return value is T ? value : null; - } - - void dispose() { - _cellByFieldId.clear(); - } -} 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 deleted file mode 100644 index d9009b2ba0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller.dart +++ /dev/null @@ -1,252 +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/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/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:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'cell_cache.dart'; -import 'cell_data_loader.dart'; -import 'cell_data_persistence.dart'; - -part 'cell_controller.freezed.dart'; - -@freezed -class CellContext with _$CellContext { - const factory CellContext({ - required String fieldId, - required RowId rowId, - }) = _DatabaseCellContext; -} - -/// [CellController] is used to manipulate the cell and receive notifications. -/// The cell data is stored in the [RowCache]'s [CellMemCache]. -/// -/// * Read/write cell data -/// * Listen on field/cell notifications. -/// -/// T represents the type of the cell data. -/// D represents the type of data that will be saved to the disk. -class CellController { - CellController({ - required this.viewId, - required FieldController fieldController, - required CellContext cellContext, - required RowCache rowCache, - required CellDataLoader cellDataLoader, - required CellDataPersistence cellDataPersistence, - }) : _fieldController = fieldController, - _cellContext = cellContext, - _rowCache = rowCache, - _cellDataLoader = cellDataLoader, - _cellDataPersistence = cellDataPersistence, - _cellDataNotifier = - CellDataNotifier(value: rowCache.cellCache.get(cellContext)) { - _startListening(); - } - - final String viewId; - final FieldController _fieldController; - final CellContext _cellContext; - final RowCache _rowCache; - final CellDataLoader _cellDataLoader; - final CellDataPersistence _cellDataPersistence; - - CellListener? _cellListener; - CellDataNotifier? _cellDataNotifier; - - Timer? _loadDataOperation; - Timer? _saveDataOperation; - - Completer? _completer; - - RowId get rowId => _cellContext.rowId; - String get fieldId => _cellContext.fieldId; - 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; - CellMemCache get _cellCache => _rowCache.cellCache; - - /// casting method for painless type coersion - CellController as() => this as CellController; - - /// Start listening to backend changes - void _startListening() { - _cellListener = CellListener( - rowId: _cellContext.rowId, - fieldId: _cellContext.fieldId, - ); - - // 1. Listen on user edit event and load the new cell data if needed. - // For example: - // user input: 12 - // cell display: $12 - _cellListener?.start( - onCellChanged: (result) { - result.fold( - (_) => _loadData(), - (err) => Log.error(err), - ); - }, - ); - - // 2. Listen on the field event and load the cell data if needed. - _fieldController.addSingleFieldListener( - fieldId, - onFieldChanged: _onFieldChangedListener, - ); - } - - /// Add a new listener - VoidCallback? addListener({ - required void Function(T?) onCellChanged, - void Function(FieldInfo fieldInfo)? onFieldChanged, - }) { - /// an adaptor for the onCellChanged listener - void onCellChangedFn() => onCellChanged(_cellDataNotifier?.value); - _cellDataNotifier?.addListener(onCellChangedFn); - - if (onFieldChanged != null) { - _fieldController.addSingleFieldListener( - fieldId, - onFieldChanged: onFieldChanged, - ); - } - - // Return the function pointer that can be used when calling removeListener. - return onCellChangedFn; - } - - void removeListener({ - required VoidCallback onCellChanged, - void Function(FieldInfo fieldInfo)? onFieldChanged, - VoidCallback? onRowMetaChanged, - }) { - _cellDataNotifier?.removeListener(onCellChanged); - - if (onFieldChanged != null) { - _fieldController.removeSingleFieldListener( - fieldId: fieldId, - onFieldChanged: onFieldChanged, - ); - } - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - // reloadOnFieldChanged should be true if you want to reload the cell - // data when the corresponding field is changed. - // For example: - // ¥12 -> $12 - if (_cellDataLoader.reloadOnFieldChange) { - _loadData(); - } - } - - /// Get the cell data. The cell data will be read from the cache first, - /// and load from disk if it doesn't exist. You can set [loadIfNotExist] to - /// false to disable this behavior. - T? getCellData({bool loadIfNotExist = true}) { - final T? data = _cellCache.get(_cellContext); - if (data == null && loadIfNotExist) { - _loadData(); - } - return data; - } - - /// Return the TypeOptionPB that can be parsed into corresponding class using the [parser]. - /// [PD] is the type that the parser return. - PD getTypeOption(TypeOptionParser parser) { - return parser.fromBuffer(fieldInfo.field.typeOptionData); - } - - /// Saves the cell data to disk. You can set [debounce] to reduce the amount - /// of save operations, which is useful when editing a [TextField]. - Future saveCellData( - D data, { - bool debounce = false, - void Function(FlowyError?)? onFinish, - }) async { - _loadDataOperation?.cancel(); - if (debounce) { - _saveDataOperation?.cancel(); - _completer = Completer(); - _saveDataOperation = Timer(const Duration(milliseconds: 300), () async { - final result = await _cellDataPersistence.save( - viewId: viewId, - cellContext: _cellContext, - data: data, - ); - onFinish?.call(result); - _completer?.complete(); - }); - } else { - final result = await _cellDataPersistence.save( - viewId: viewId, - cellContext: _cellContext, - data: data, - ); - onFinish?.call(result); - } - } - - void _loadData() { - _saveDataOperation?.cancel(); - _loadDataOperation?.cancel(); - - _loadDataOperation = Timer(const Duration(milliseconds: 10), () { - _cellDataLoader - .loadData(viewId: viewId, cellContext: _cellContext) - .then((data) { - if (data != null) { - _cellCache.insert(_cellContext, data); - } else { - _cellCache.remove(_cellContext); - } - _cellDataNotifier?.value = data; - }); - }); - } - - Future dispose() async { - await _cellListener?.stop(); - _cellListener = null; - - _fieldController.removeSingleFieldListener( - fieldId: fieldId, - onFieldChanged: _onFieldChangedListener, - ); - - _loadDataOperation?.cancel(); - await _completer?.future; - _saveDataOperation?.cancel(); - _cellDataNotifier?.dispose(); - _cellDataNotifier = null; - } -} - -class CellDataNotifier extends ChangeNotifier { - CellDataNotifier({required T value, this.listenWhen}) : _value = value; - - T _value; - bool Function(T? oldValue, T? newValue)? listenWhen; - - set value(T newValue) { - if (listenWhen != null && !listenWhen!.call(_value, newValue)) { - return; - } - _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 deleted file mode 100644 index afe05e8b70..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -import 'cell_controller.dart'; -import 'cell_data_loader.dart'; -import 'cell_data_persistence.dart'; - -typedef TextCellController = CellController; -typedef CheckboxCellController = CellController; -typedef NumberCellController = CellController; -typedef SelectOptionCellController - = CellController; -typedef ChecklistCellController = CellController; -typedef DateCellController = CellController; -typedef TimestampCellController = CellController; -typedef URLCellController = CellController; -typedef RelationCellController = CellController; -typedef SummaryCellController = CellController; -typedef TimeCellController = CellController; -typedef TranslateCellController = CellController; -typedef MediaCellController = CellController; - -CellController makeCellController( - DatabaseController databaseController, - CellContext cellContext, -) { - final DatabaseController(:viewId, :rowCache, :fieldController) = - databaseController; - final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType; - switch (fieldType) { - case FieldType.Checkbox: - return CheckboxCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: CheckboxCellDataParser(), - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.DateTime: - return DateCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: DateCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - return TimestampCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: TimestampCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.Number: - return NumberCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: NumberCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.RichText: - return TextCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: StringCellDataParser(), - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.MultiSelect: - case FieldType.SingleSelect: - return SelectOptionCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: SelectOptionCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.Checklist: - return ChecklistCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: ChecklistCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.URL: - return URLCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: URLCellDataParser(), - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.Relation: - return RelationCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: RelationCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.Summary: - return SummaryCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: StringCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.Time: - return TimeCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: TimeCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); - case FieldType.Translate: - return TranslateCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: StringCellDataParser(), - reloadOnFieldChange: true, - ), - 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 deleted file mode 100644 index cfab4668ae..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/plugins/database/domain/cell_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -import 'cell_controller.dart'; - -abstract class CellDataParser { - T? parserData(List data); -} - -class CellDataLoader { - CellDataLoader({ - required this.parser, - this.reloadOnFieldChange = false, - }); - - final CellDataParser parser; - - /// Reload the cell data if the field is changed. - final bool reloadOnFieldChange; - - Future loadData({ - required String viewId, - required CellContext cellContext, - }) { - return CellBackendService.getCell( - viewId: viewId, - cellContext: cellContext, - ).then( - (result) => result.fold( - (CellPB cell) { - try { - return parser.parserData(cell.data); - } catch (e, s) { - Log.error('$parser parser cellData failed, $e'); - Log.error('Stack trace \n $s'); - return null; - } - }, - (err) { - Log.error(err); - return null; - }, - ), - ); - } -} - -class StringCellDataParser implements CellDataParser { - @override - String? parserData(List data) { - try { - final s = utf8.decode(data); - return s; - } catch (e) { - Log.error("Failed to parse string data: $e"); - return null; - } - } -} - -class CheckboxCellDataParser implements CellDataParser { - @override - CheckboxCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - - try { - return CheckboxCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse checkbox data: $e"); - return null; - } - } -} - -class NumberCellDataParser implements CellDataParser { - @override - String? parserData(List data) { - try { - return utf8.decode(data); - } catch (e) { - Log.error("Failed to parse number data: $e"); - return null; - } - } -} - -class DateCellDataParser implements CellDataParser { - @override - DateCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - try { - return DateCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse date data: $e"); - return null; - } - } -} - -class TimestampCellDataParser implements CellDataParser { - @override - TimestampCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - try { - return TimestampCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse timestamp data: $e"); - return null; - } - } -} - -class SelectOptionCellDataParser - implements CellDataParser { - @override - SelectOptionCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - try { - return SelectOptionCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse select option data: $e"); - return null; - } - } -} - -class ChecklistCellDataParser implements CellDataParser { - @override - ChecklistCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - - try { - return ChecklistCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse checklist data: $e"); - return null; - } - } -} - -class URLCellDataParser implements CellDataParser { - @override - URLCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - try { - return URLCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse url data: $e"); - return null; - } - } -} - -class RelationCellDataParser implements CellDataParser { - @override - RelationCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - - try { - return RelationCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse relation data: $e"); - return null; - } - } -} - -class TimeCellDataParser implements CellDataParser { - @override - TimeCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - try { - return TimeCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse timer data: $e"); - return null; - } - } -} - -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/cell/cell_data_persistence.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_persistence.dart deleted file mode 100644 index f377515ac4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_persistence.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:appflowy/plugins/database/domain/cell_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; - -import 'cell_controller.dart'; - -/// Save the cell data to disk -/// You can extend this class to do custom operations. -abstract class CellDataPersistence { - Future save({ - required String viewId, - required CellContext cellContext, - required D data, - }); -} - -class TextCellDataPersistence implements CellDataPersistence { - TextCellDataPersistence(); - - @override - Future save({ - required String viewId, - required CellContext cellContext, - required String data, - }) async { - final fut = CellBackendService.updateCell( - viewId: viewId, - cellContext: cellContext, - data: data, - ); - return fut.then((result) { - return result.fold( - (l) => null, - (err) => err, - ); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart deleted file mode 100644 index 5317539128..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart +++ /dev/null @@ -1,407 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database/application/view/view_cache.dart'; -import 'package:appflowy/plugins/database/domain/database_view_service.dart'; -import 'package:appflowy/plugins/database/domain/group_listener.dart'; -import 'package:appflowy/plugins/database/domain/layout_service.dart'; -import 'package:appflowy/plugins/database/domain/layout_setting_listener.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-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -import 'defines.dart'; -import 'row/row_cache.dart'; - -typedef OnGroupConfigurationChanged = void Function(List); -typedef OnGroupByField = void Function(List); -typedef OnUpdateGroup = void Function(List); -typedef OnDeleteGroup = void Function(List); -typedef OnInsertGroup = void Function(InsertedGroupPB); - -class GroupCallbacks { - GroupCallbacks({ - this.onGroupConfigurationChanged, - this.onGroupByField, - this.onUpdateGroup, - this.onDeleteGroup, - this.onInsertGroup, - }); - - final OnGroupConfigurationChanged? onGroupConfigurationChanged; - final OnGroupByField? onGroupByField; - final OnUpdateGroup? onUpdateGroup; - final OnDeleteGroup? onDeleteGroup; - final OnInsertGroup? onInsertGroup; -} - -class DatabaseLayoutSettingCallbacks { - DatabaseLayoutSettingCallbacks({ - required this.onLayoutSettingsChanged, - }); - - final void Function(DatabaseLayoutSettingPB) onLayoutSettingsChanged; -} - -class DatabaseCallbacks { - DatabaseCallbacks({ - this.onDatabaseChanged, - this.onNumOfRowsChanged, - this.onFieldsChanged, - this.onFiltersChanged, - this.onSortsChanged, - this.onRowsUpdated, - this.onRowsDeleted, - this.onRowsCreated, - }); - - OnDatabaseChanged? onDatabaseChanged; - OnFieldsChanged? onFieldsChanged; - OnFiltersChanged? onFiltersChanged; - OnSortsChanged? onSortsChanged; - OnNumOfRowsChanged? onNumOfRowsChanged; - OnRowsDeleted? onRowsDeleted; - OnRowsUpdated? onRowsUpdated; - OnRowsCreated? onRowsCreated; -} - -class DatabaseController { - DatabaseController({required this.view}) - : _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id), - fieldController = FieldController(viewId: view.id), - _groupListener = DatabaseGroupListener(view.id), - databaseLayout = databaseLayoutFromViewLayout(view.layout), - _layoutListener = DatabaseLayoutSettingListener(view.id) { - _viewCache = DatabaseViewCache( - viewId: viewId, - fieldController: fieldController, - ); - - _listenOnRowsChanged(); - _listenOnFieldsChanged(); - _listenOnGroupChanged(); - _listenOnLayoutChanged(); - } - - final ViewPB view; - final DatabaseViewBackendService _databaseViewBackendSvc; - final FieldController fieldController; - DatabaseLayoutPB databaseLayout; - DatabaseLayoutSettingPB? databaseLayoutSetting; - late DatabaseViewCache _viewCache; - - // Callbacks - final List _databaseCallbacks = []; - final List _groupCallbacks = []; - final List _layoutCallbacks = []; - final Set> _compactModeCallbacks = {}; - - // Getters - RowCache get rowCache => _viewCache.rowCache; - - String get viewId => view.id; - - // Listener - final DatabaseGroupListener _groupListener; - 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); - } - } - - ValueNotifier get compactModeNotifier => _compactMode; - - void addListener({ - DatabaseCallbacks? onDatabaseChanged, - DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged, - GroupCallbacks? onGroupChanged, - ValueChanged? onCompactModeChanged, - }) { - if (onLayoutSettingsChanged != null) { - _layoutCallbacks.add(onLayoutSettingsChanged); - } - - if (onDatabaseChanged != null) { - _databaseCallbacks.add(onDatabaseChanged); - } - - 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 { - return _databaseViewBackendSvc.openDatabase().then((result) { - return result.fold( - (DatabasePB database) async { - databaseLayout = database.layoutType; - - // Load the actual database field data. - final fieldsOrFail = await fieldController.loadFields( - fieldIds: database.fields, - ); - return fieldsOrFail.fold( - (fields) { - // Notify the database is changed after the fields are loaded. - // The database won't can't be used until the fields are loaded. - for (final callback in _databaseCallbacks) { - callback.onDatabaseChanged?.call(database); - } - _viewCache.rowCache.setInitialRows(database.rows); - return Future(() async { - await _loadGroups(); - await _loadLayoutSetting(); - return FlowyResult.success(fields); - }); - }, - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - }, - (err) => FlowyResult.failure(err), - ); - }); - } - - Future> moveGroupRow({ - required RowMetaPB fromRow, - required String fromGroupId, - required String toGroupId, - RowMetaPB? toRow, - }) { - return _databaseViewBackendSvc.moveGroupRow( - fromRowId: fromRow.id, - fromGroupId: fromGroupId, - toGroupId: toGroupId, - toRowId: toRow?.id, - ); - } - - Future> moveRow({ - required String fromRowId, - required String toRowId, - }) { - return _databaseViewBackendSvc.moveRow( - fromRowId: fromRowId, - toRowId: toRowId, - ); - } - - Future> moveGroup({ - required String fromGroupId, - required String toGroupId, - }) { - return _databaseViewBackendSvc.moveGroup( - fromGroupId: fromGroupId, - toGroupId: toGroupId, - ); - } - - Future updateLayoutSetting({ - BoardLayoutSettingPB? boardLayoutSetting, - CalendarLayoutSettingPB? calendarLayoutSetting, - }) async { - await _databaseViewBackendSvc - .updateLayoutSetting( - boardLayoutSetting: boardLayoutSetting, - calendarLayoutSetting: calendarLayoutSetting, - layoutType: databaseLayout, - ) - .then((result) { - result.fold((l) => null, (r) => Log.error(r)); - }); - } - - Future dispose() async { - await _databaseViewBackendSvc.closeView(); - await fieldController.dispose(); - await _groupListener.stop(); - await _viewCache.dispose(); - _databaseCallbacks.clear(); - _groupCallbacks.clear(); - _layoutCallbacks.clear(); - _compactModeCallbacks.clear(); - _isLoading.dispose(); - } - - Future _loadGroups() async { - final groupsResult = await _databaseViewBackendSvc.loadGroups(); - groupsResult.fold( - (groups) { - for (final callback in _groupCallbacks) { - callback.onGroupByField?.call(groups.items); - } - }, - (err) => Log.error(err), - ); - } - - Future _loadLayoutSetting() { - return _databaseViewBackendSvc - .getLayoutSetting(databaseLayout) - .then((result) { - result.fold( - (newDatabaseLayoutSetting) { - databaseLayoutSetting = newDatabaseLayoutSetting; - - for (final callback in _layoutCallbacks) { - callback.onLayoutSettingsChanged(newDatabaseLayoutSetting); - } - }, - (r) => Log.error(r), - ); - }); - } - - void _listenOnRowsChanged() { - final callbacks = DatabaseViewCallbacks( - onNumOfRowsChanged: (rows, rowByRowId, reason) { - for (final callback in _databaseCallbacks) { - callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason); - } - }, - onRowsDeleted: (ids) { - for (final callback in _databaseCallbacks) { - callback.onRowsDeleted?.call(ids); - } - }, - onRowsUpdated: (ids, reason) { - for (final callback in _databaseCallbacks) { - callback.onRowsUpdated?.call(ids, reason); - } - }, - onRowsCreated: (ids) { - for (final callback in _databaseCallbacks) { - callback.onRowsCreated?.call(ids); - } - }, - ); - _viewCache.addListener(callbacks); - } - - void _listenOnFieldsChanged() { - fieldController.addListener( - onReceiveFields: (fields) { - for (final callback in _databaseCallbacks) { - callback.onFieldsChanged?.call(UnmodifiableListView(fields)); - } - }, - onSorts: (sorts) { - for (final callback in _databaseCallbacks) { - callback.onSortsChanged?.call(sorts); - } - }, - onFilters: (filters) { - for (final callback in _databaseCallbacks) { - callback.onFiltersChanged?.call(filters); - } - }, - ); - } - - void _listenOnGroupChanged() { - _groupListener.start( - onNumOfGroupsChanged: (result) { - result.fold( - (changeset) { - if (changeset.updateGroups.isNotEmpty) { - for (final callback in _groupCallbacks) { - callback.onUpdateGroup?.call(changeset.updateGroups); - } - } - - if (changeset.deletedGroups.isNotEmpty) { - for (final callback in _groupCallbacks) { - callback.onDeleteGroup?.call(changeset.deletedGroups); - } - } - - for (final insertedGroup in changeset.insertedGroups) { - for (final callback in _groupCallbacks) { - callback.onInsertGroup?.call(insertedGroup); - } - } - }, - (r) => Log.error(r), - ); - }, - onGroupByNewField: (result) { - result.fold( - (groups) { - for (final callback in _groupCallbacks) { - callback.onGroupByField?.call(groups); - } - }, - (r) => Log.error(r), - ); - }, - ); - } - - void _listenOnLayoutChanged() { - _layoutListener.start( - onLayoutChanged: (result) { - result.fold( - (newLayout) { - databaseLayoutSetting = newLayout; - databaseLayoutSetting?.freeze(); - - for (final callback in _layoutCallbacks) { - callback.onLayoutSettingsChanged(newLayout); - } - }, - (r) => Log.error(r), - ); - }, - ); - } - - 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 deleted file mode 100644 index 65deae7e58..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:collection'; - -import 'package:appflowy/plugins/database/application/field/filter_entities.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: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 OnDatabaseChanged = void Function(DatabasePB); - -typedef OnRowsCreated = void Function(List rows); -typedef OnRowsUpdated = void Function( - List rowIds, - ChangedReason reason, -); -typedef OnRowsDeleted = void Function(List rowIds); -typedef OnNumOfRowsChanged = void Function( - UnmodifiableListView rows, - UnmodifiableMapView rowById, - ChangedReason reason, -); -typedef OnRowsVisibilityChanged = void Function( - List<(RowId, bool)> rowVisibilityChanges, -); - -@freezed -class LoadingState with _$LoadingState { - const factory LoadingState.idle() = _Idle; - const factory LoadingState.loading() = _Loading; - const factory LoadingState.finish( - FlowyResult successOrFail, - ) = _Finish; - - const LoadingState._(); - bool isLoading() => this is _Loading; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_cell_bloc.dart deleted file mode 100644 index 5a72fcccc0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_cell_bloc.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'field_info.dart'; - -part 'field_cell_bloc.freezed.dart'; - -class FieldCellBloc extends Bloc { - FieldCellBloc({required String viewId, required FieldInfo fieldInfo}) - : _fieldSettingsService = FieldSettingsBackendService(viewId: viewId), - super(FieldCellState.initial(fieldInfo)) { - _dispatch(); - } - - final FieldSettingsBackendService _fieldSettingsService; - - void _dispatch() { - on( - (event, emit) async { - event.when( - onFieldChanged: (newFieldInfo) => - emit(FieldCellState.initial(newFieldInfo)), - onResizeStart: () => - emit(state.copyWith(isResizing: true, resizeStart: state.width)), - startUpdateWidth: (offset) { - final width = max(offset + state.resizeStart, 50).toDouble(); - emit(state.copyWith(width: width)); - }, - endUpdateWidth: () { - if (state.width != state.fieldInfo.width) { - _fieldSettingsService.updateFieldSettings( - fieldId: state.fieldInfo.id, - width: state.width, - ); - } - emit(state.copyWith(isResizing: false, resizeStart: 0)); - }, - ); - }, - ); - } -} - -@freezed -class FieldCellEvent with _$FieldCellEvent { - const factory FieldCellEvent.onFieldChanged(FieldInfo newFieldInfo) = - _OnFieldChanged; - const factory FieldCellEvent.onResizeStart() = _OnResizeStart; - const factory FieldCellEvent.startUpdateWidth(double offset) = - _StartUpdateWidth; - const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth; -} - -@freezed -class FieldCellState with _$FieldCellState { - factory FieldCellState.initial(FieldInfo fieldInfo) => FieldCellState( - fieldInfo: fieldInfo, - isResizing: false, - width: fieldInfo.width!.toDouble(), - resizeStart: 0, - ); - - const factory FieldCellState({ - required FieldInfo fieldInfo, - required double width, - required bool isResizing, - required double resizeStart, - }) = _FieldCellState; -} 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 deleted file mode 100644 index 93fd69bcfc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart +++ /dev/null @@ -1,773 +0,0 @@ -import 'dart:collection'; - -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'; -import 'package:appflowy/plugins/database/domain/field_listener.dart'; -import 'package:appflowy/plugins/database/domain/field_settings_listener.dart'; -import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; -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_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 = []; - - set fieldInfos(List fieldInfos) { - _fieldInfos = fieldInfos; - notifyListeners(); - } - - void notify() { - notifyListeners(); - } - - UnmodifiableListView get fieldInfos => - UnmodifiableListView(_fieldInfos); -} - -class _GridFilterNotifier extends ChangeNotifier { - List _filters = []; - - set filters(List filters) { - _filters = filters; - notifyListeners(); - } - - void notify() { - notifyListeners(); - } - - List get filters => _filters; -} - -class _GridSortNotifier extends ChangeNotifier { - List _sorts = []; - - set sorts(List sorts) { - _sorts = sorts; - notifyListeners(); - } - - void notify() { - notifyListeners(); - } - - 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); - -class FieldController { - FieldController({required this.viewId}) - : _fieldListener = FieldsListener(viewId: viewId), - _settingListener = DatabaseSettingListener(viewId: viewId), - _filterBackendSvc = FilterBackendService(viewId: viewId), - _filtersListener = FiltersListener(viewId: viewId), - _databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId), - _sortBackendSvc = SortBackendService(viewId: viewId), - _sortsListener = SortsListener(viewId: viewId), - _fieldSettingsListener = FieldSettingsListener(viewId: viewId), - _fieldSettingsBackendSvc = FieldSettingsBackendService(viewId: viewId) { - // Start listeners - _listenOnFieldChanges(); - _listenOnSettingChanges(); - _listenOnFilterChanges(); - _listenOnSortChanged(); - _listenOnFieldSettingsChanged(); - } - - final String viewId; - - // Listeners - final FieldsListener _fieldListener; - final DatabaseSettingListener _settingListener; - final FiltersListener _filtersListener; - final SortsListener _sortsListener; - final FieldSettingsListener _fieldSettingsListener; - - // FFI services - final DatabaseViewBackendService _databaseViewBackendSvc; - final FilterBackendService _filterBackendSvc; - final SortBackendService _sortBackendSvc; - final FieldSettingsBackendService _fieldSettingsBackendSvc; - - bool _isDisposed = false; - - // Field callbacks - final Map _fieldCallbacks = {}; - final _GridFieldNotifier _fieldNotifier = _GridFieldNotifier(); - - // Field updated callbacks - final Map)> - _updatedFieldCallbacks = {}; - - // Filter callbacks - final Map _filterCallbacks = {}; - _GridFilterNotifier? _filterNotifier = _GridFilterNotifier(); - - // Sort callbacks - final Map _sortCallbacks = {}; - _GridSortNotifier? _sortNotifier = _GridSortNotifier(); - - // Database settings temporary storage - final Map _groupConfigurationByFieldId = {}; - final List _fieldSettings = []; - - // Getters - List get fieldInfos => [..._fieldNotifier.fieldInfos]; - List get filters => [..._filterNotifier?.filters ?? []]; - List get sorts => [..._sortNotifier?.sorts ?? []]; - List get groupSettings => - _groupConfigurationByFieldId.entries.map((e) => e.value).toList(); - - FieldInfo? getField(String fieldId) { - return _fieldNotifier.fieldInfos - .firstWhereOrNull((element) => element.id == fieldId); - } - - DatabaseFilter? getFilterByFilterId(String filterId) { - return _filterNotifier?.filters - .firstWhereOrNull((element) => element.filterId == filterId); - } - - DatabaseFilter? getFilterByFieldId(String fieldId) { - return _filterNotifier?.filters - .firstWhereOrNull((element) => element.fieldId == fieldId); - } - - DatabaseSort? getSortBySortId(String sortId) { - return _sortNotifier?.sorts - .firstWhereOrNull((element) => element.sortId == sortId); - } - - DatabaseSort? getSortByFieldId(String fieldId) { - return _sortNotifier?.sorts - .firstWhereOrNull((element) => element.fieldId == fieldId); - } - - /// Listen for filter changes in the backend. - void _listenOnFilterChanges() { - _filtersListener.start( - onFilterChanged: (result) { - if (_isDisposed) { - return; - } - - result.fold( - (FilterChangesetNotificationPB changeset) { - _filterNotifier?.filters = - _filterListFromPBs(changeset.filters.items); - _fieldNotifier.fieldInfos = - _updateFieldInfos(_fieldNotifier.fieldInfos); - }, - (err) => Log.error(err), - ); - }, - ); - } - - /// Listen for sort changes in the backend. - void _listenOnSortChanged() { - void deleteSortFromChangeset( - List newDatabaseSorts, - SortChangesetNotificationPB changeset, - ) { - final deleteSortIds = changeset.deleteSorts.map((e) => e.id).toList(); - if (deleteSortIds.isNotEmpty) { - newDatabaseSorts.retainWhere( - (element) => !deleteSortIds.contains(element.sortId), - ); - } - } - - void insertSortFromChangeset( - List newDatabaseSorts, - SortChangesetNotificationPB changeset, - ) { - for (final newSortPB in changeset.insertSorts) { - final sortIndex = newDatabaseSorts - .indexWhere((element) => element.sortId == newSortPB.sort.id); - if (sortIndex == -1) { - newDatabaseSorts.insert( - newSortPB.index, - DatabaseSort.fromPB(newSortPB.sort), - ); - } - } - } - - void updateSortFromChangeset( - List newDatabaseSorts, - SortChangesetNotificationPB changeset, - ) { - for (final updatedSort in changeset.updateSorts) { - final newDatabaseSort = DatabaseSort.fromPB(updatedSort); - - final sortIndex = newDatabaseSorts.indexWhere( - (element) => element.sortId == updatedSort.id, - ); - - if (sortIndex != -1) { - newDatabaseSorts.removeAt(sortIndex); - newDatabaseSorts.insert(sortIndex, newDatabaseSort); - } else { - newDatabaseSorts.add(newDatabaseSort); - } - } - } - - void updateFieldInfos( - List newDatabaseSorts, - SortChangesetNotificationPB changeset, - ) { - final changedFieldIds = HashSet.from([ - ...changeset.insertSorts.map((sort) => sort.sort.fieldId), - ...changeset.updateSorts.map((sort) => sort.fieldId), - ...changeset.deleteSorts.map((sort) => sort.fieldId), - ...?_sortNotifier?.sorts.map((sort) => sort.fieldId), - ]); - - final newFieldInfos = [...fieldInfos]; - - for (final fieldId in changedFieldIds) { - final index = - newFieldInfos.indexWhere((fieldInfo) => fieldInfo.id == fieldId); - if (index == -1) { - continue; - } - newFieldInfos[index] = newFieldInfos[index].copyWith( - hasSort: newDatabaseSorts.any((sort) => sort.fieldId == fieldId), - ); - } - - _fieldNotifier.fieldInfos = newFieldInfos; - } - - _sortsListener.start( - onSortChanged: (result) { - if (_isDisposed) { - return; - } - result.fold( - (SortChangesetNotificationPB changeset) { - final List newDatabaseSorts = sorts; - deleteSortFromChangeset(newDatabaseSorts, changeset); - insertSortFromChangeset(newDatabaseSorts, changeset); - updateSortFromChangeset(newDatabaseSorts, changeset); - - updateFieldInfos(newDatabaseSorts, changeset); - _sortNotifier?.sorts = newDatabaseSorts; - }, - (err) => Log.error(err), - ); - }, - ); - } - - /// Listen for database setting changes in the backend. - void _listenOnSettingChanges() { - _settingListener.start( - onSettingUpdated: (result) { - if (_isDisposed) { - return; - } - - result.fold( - (setting) => _updateSetting(setting), - (r) => Log.error(r), - ); - }, - ); - } - - /// Listen for field changes in the backend. - void _listenOnFieldChanges() { - Future attachFieldSettings(FieldInfo fieldInfo) async { - return _fieldSettingsBackendSvc - .getFieldSettings(fieldInfo.id) - .then((result) { - final fieldSettings = result.fold( - (fieldSettings) => fieldSettings, - (err) => null, - ); - if (fieldSettings == null) { - return fieldInfo; - } - final updatedFieldInfo = - fieldInfo.copyWith(fieldSettings: fieldSettings); - - final index = _fieldSettings - .indexWhere((element) => element.fieldId == fieldInfo.id); - if (index != -1) { - _fieldSettings.removeAt(index); - } - _fieldSettings.add(fieldSettings); - - return updatedFieldInfo; - }); - } - - List deleteFields(List deletedFields) { - if (deletedFields.isEmpty) { - return fieldInfos; - } - final List newFields = fieldInfos; - final Map deletedFieldMap = { - for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder, - }; - - newFields.retainWhere((field) => deletedFieldMap[field.id] == null); - return newFields; - } - - Future> insertFields( - List insertedFields, - List fieldInfos, - ) async { - if (insertedFields.isEmpty) { - return fieldInfos; - } - final List newFieldInfos = fieldInfos; - for (final indexField in insertedFields) { - final initial = FieldInfo.initial(indexField.field_1); - final fieldInfo = await attachFieldSettings(initial); - if (newFieldInfos.length > indexField.index) { - newFieldInfos.insert(indexField.index, fieldInfo); - } else { - newFieldInfos.add(fieldInfo); - } - } - return newFieldInfos; - } - - Future<(List, List)> updateFields( - List updatedFieldPBs, - List fieldInfos, - ) async { - if (updatedFieldPBs.isEmpty) { - return ([], fieldInfos); - } - - final List newFieldInfo = fieldInfos; - final List updatedFields = []; - for (final updatedFieldPB in updatedFieldPBs) { - final index = - newFieldInfo.indexWhere((field) => field.id == updatedFieldPB.id); - if (index != -1) { - newFieldInfo.removeAt(index); - final initial = FieldInfo.initial(updatedFieldPB); - final fieldInfo = await attachFieldSettings(initial); - newFieldInfo.insert(index, fieldInfo); - updatedFields.add(fieldInfo); - } - } - - return (updatedFields, newFieldInfo); - } - - // Listen on field's changes - _fieldListener.start( - onFieldsChanged: (result) async { - result.fold( - (changeset) async { - if (_isDisposed) { - return; - } - List updatedFields; - List fieldInfos = deleteFields(changeset.deletedFields); - fieldInfos = - await insertFields(changeset.insertedFields, fieldInfos); - (updatedFields, fieldInfos) = - await updateFields(changeset.updatedFields, fieldInfos); - - _fieldNotifier.fieldInfos = _updateFieldInfos(fieldInfos); - for (final listener in _updatedFieldCallbacks.values) { - listener(updatedFields); - } - }, - (err) => Log.error(err), - ); - }, - ); - } - - /// Listen for field setting changes in the backend. - void _listenOnFieldSettingsChanged() { - FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) { - final newFields = [...fieldInfos]; - - if (newFields.isEmpty) { - return null; - } - - 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]; - } - - return null; - } - - _fieldSettingsListener.start( - onFieldSettingsChanged: (result) { - if (_isDisposed) { - return; - } - result.fold( - (fieldSettings) { - final updatedFieldInfo = updateFieldSettings(fieldSettings); - if (updatedFieldInfo == null) { - return; - } - - for (final listener in _updatedFieldCallbacks.values) { - listener([updatedFieldInfo]); - } - }, - (err) => Log.error(err), - ); - }, - ); - } - - /// Updates sort, filter, group and field info from `DatabaseViewSettingPB` - void _updateSetting(DatabaseViewSettingPB setting) { - _groupConfigurationByFieldId.clear(); - for (final configuration in setting.groupSettings.items) { - _groupConfigurationByFieldId[configuration.fieldId] = configuration; - } - - _filterNotifier?.filters = _filterListFromPBs(setting.filters.items); - - _sortNotifier?.sorts = _sortListFromPBs(setting.sorts.items); - - _fieldSettings.clear(); - _fieldSettings.addAll(setting.fieldSettings.items); - - _fieldNotifier.fieldInfos = _updateFieldInfos(_fieldNotifier.fieldInfos); - } - - /// 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(); - } - - /// Load all of the fields. This is required when opening the database - Future> loadFields({ - required List fieldIds, - }) async { - final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds); - return Future( - () => result.fold( - (newFields) async { - if (_isDisposed) { - return FlowyResult.success(null); - } - - _fieldNotifier.fieldInfos = - newFields.map((field) => FieldInfo.initial(field)).toList(); - await Future.wait([ - _loadFilters(), - _loadSorts(), - _loadAllFieldSettings(), - _loadSettings(), - ]); - _fieldNotifier.fieldInfos = - _updateFieldInfos(_fieldNotifier.fieldInfos); - - return FlowyResult.success(null); - }, - (err) => FlowyResult.failure(err), - ), - ); - } - - /// Load all the filters from the backend. Required by `loadFields` - Future> _loadFilters() async { - return _filterBackendSvc.getAllFilters().then((result) { - return result.fold( - (filterPBs) { - _filterNotifier?.filters = _filterListFromPBs(filterPBs); - return FlowyResult.success(null); - }, - (err) => FlowyResult.failure(err), - ); - }); - } - - /// Load all the sorts from the backend. Required by `loadFields` - Future> _loadSorts() async { - return _sortBackendSvc.getAllSorts().then((result) { - return result.fold( - (sortPBs) { - _sortNotifier?.sorts = _sortListFromPBs(sortPBs); - return FlowyResult.success(null); - }, - (err) => FlowyResult.failure(err), - ); - }); - } - - /// Load all the field settings from the backend. Required by `loadFields` - Future> _loadAllFieldSettings() async { - return _fieldSettingsBackendSvc.getAllFieldSettings().then((result) { - return result.fold( - (fieldSettingsList) { - _fieldSettings.clear(); - _fieldSettings.addAll(fieldSettingsList); - return FlowyResult.success(null); - }, - (err) => FlowyResult.failure(err), - ); - }); - } - - Future> _loadSettings() async { - return SettingBackendService(viewId: viewId).getSetting().then( - (result) => result.fold( - (setting) { - _groupConfigurationByFieldId.clear(); - for (final configuration in setting.groupSettings.items) { - _groupConfigurationByFieldId[configuration.fieldId] = - configuration; - } - return FlowyResult.success(null); - }, - (err) => FlowyResult.failure(err), - ), - ); - } - - /// Attach corresponding `FieldInfo`s to the `FilterPB`s - List _filterListFromPBs(List filterPBs) { - return filterPBs.map(DatabaseFilter.fromPB).toList(); - } - - /// Attach corresponding `FieldInfo`s to the `SortPB`s - List _sortListFromPBs(List sortPBs) { - return sortPBs.map(DatabaseSort.fromPB).toList(); - } - - void addListener({ - OnReceiveFields? onReceiveFields, - OnReceiveUpdateFields? onFieldsChanged, - OnReceiveFilters? onFilters, - OnReceiveSorts? onSorts, - bool Function()? listenWhen, - }) { - if (onFieldsChanged != null) { - void callback(List updateFields) { - if (listenWhen != null && listenWhen() == false) { - return; - } - onFieldsChanged(updateFields); - } - - _updatedFieldCallbacks[onFieldsChanged] = callback; - } - - if (onReceiveFields != null) { - void callback() { - if (listenWhen != null && listenWhen() == false) { - return; - } - onReceiveFields(fieldInfos); - } - - _fieldCallbacks[onReceiveFields] = callback; - _fieldNotifier.addListener(callback); - } - - if (onFilters != null) { - void callback() { - if (listenWhen != null && listenWhen() == false) { - return; - } - onFilters(filters); - } - - _filterCallbacks[onFilters] = callback; - _filterNotifier?.addListener(callback); - } - - if (onSorts != null) { - void callback() { - if (listenWhen != null && listenWhen() == false) { - return; - } - onSorts(sorts); - } - - _sortCallbacks[onSorts] = callback; - _sortNotifier?.addListener(callback); - } - } - - void addSingleFieldListener( - String fieldId, { - required OnReceiveField onFieldChanged, - bool Function()? listenWhen, - }) { - void key(List fieldInfos) { - final fieldInfo = fieldInfos.firstWhereOrNull( - (fieldInfo) => fieldInfo.id == fieldId, - ); - if (fieldInfo != null) { - onFieldChanged(fieldInfo); - } - } - - void callback() { - if (listenWhen != null && listenWhen() == false) { - return; - } - key(fieldInfos); - } - - _fieldCallbacks[key] = callback; - _fieldNotifier.addListener(callback); - } - - void removeListener({ - OnReceiveFields? onFieldsListener, - OnReceiveSorts? onSortsListener, - OnReceiveFilters? onFiltersListener, - OnReceiveUpdateFields? onChangesetListener, - }) { - if (onFieldsListener != null) { - final callback = _fieldCallbacks.remove(onFieldsListener); - if (callback != null) { - _fieldNotifier.removeListener(callback); - } - } - if (onFiltersListener != null) { - final callback = _filterCallbacks.remove(onFiltersListener); - if (callback != null) { - _filterNotifier?.removeListener(callback); - } - } - - if (onSortsListener != null) { - final callback = _sortCallbacks.remove(onSortsListener); - if (callback != null) { - _sortNotifier?.removeListener(callback); - } - } - } - - void removeSingleFieldListener({ - required String fieldId, - required OnReceiveField onFieldChanged, - }) { - void key(List fieldInfos) { - final fieldInfo = fieldInfos.firstWhereOrNull( - (fieldInfo) => fieldInfo.id == fieldId, - ); - if (fieldInfo != null) { - onFieldChanged(fieldInfo); - } - } - - final callback = _fieldCallbacks.remove(key); - if (callback != null) { - _fieldNotifier.removeListener(callback); - } - } - - /// Stop listeners, dispose notifiers and clear listener callbacks - Future dispose() async { - if (_isDisposed) { - Log.warn('FieldController is already disposed'); - return; - } - _isDisposed = true; - await _fieldListener.stop(); - await _filtersListener.stop(); - await _settingListener.stop(); - await _sortsListener.stop(); - await _fieldSettingsListener.stop(); - - for (final callback in _fieldCallbacks.values) { - _fieldNotifier.removeListener(callback); - } - _fieldNotifier.dispose(); - - for (final callback in _filterCallbacks.values) { - _filterNotifier?.removeListener(callback); - } - _filterNotifier?.dispose(); - _filterNotifier = null; - - for (final callback in _sortCallbacks.values) { - _sortNotifier?.removeListener(callback); - } - _sortNotifier?.dispose(); - _sortNotifier = null; - } -} - -class RowCacheDependenciesImpl extends RowFieldsDelegate with RowLifeCycle { - RowCacheDependenciesImpl(FieldController cache) : _fieldController = cache; - - final FieldController _fieldController; - OnReceiveFields? _onFieldFn; - - @override - UnmodifiableListView get fieldInfos => - UnmodifiableListView(_fieldController.fieldInfos); - - @override - void onFieldsChanged(void Function(List) callback) { - if (_onFieldFn != null) { - _fieldController.removeListener(onFieldsListener: _onFieldFn!); - } - - _onFieldFn = (fieldInfos) => callback(fieldInfos); - _fieldController.addListener(onReceiveFields: _onFieldFn); - } - - @override - void onRowDisposed() { - if (_onFieldFn != null) { - _fieldController.removeListener(onFieldsListener: _onFieldFn!); - _onFieldFn = null; - } - } -} 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 deleted file mode 100644 index 1c056b1461..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart +++ /dev/null @@ -1,187 +0,0 @@ -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'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'field_controller.dart'; -import 'field_info.dart'; - -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( - viewId: viewId, - fieldId: fieldInfo.id, - ), - fieldSettingsService = FieldSettingsBackendService(viewId: viewId), - super(FieldEditorState(field: fieldInfo)) { - _dispatch(); - _startListening(); - _init(); - } - - final String viewId; - final FieldInfo fieldInfo; - final bool isNew; - final FieldController fieldController; - 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( - fieldId: fieldId, - onFieldChanged: _listener, - ); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didUpdateField: (fieldInfo) { - emit(state.copyWith(field: fieldInfo)); - }, - switchFieldType: (fieldType) async { - String? fieldName; - if (!state.wasRenameManually && isNew) { - fieldName = fieldType.i18n; - } - - await _fieldService.updateType( - fieldType: fieldType, - fieldName: fieldName, - ); - }, - 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); - _logIfError(result); - }, - updateTypeOption: (typeOptionData) async { - final result = await FieldBackendService.updateFieldTypeOption( - viewId: viewId, - fieldId: fieldId, - typeOptionData: typeOptionData, - ); - _logIfError(result); - }, - insertLeft: () async { - 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(); - result.fold( - (newField) => onFieldInserted?.call(newField.id), - (err) => Log.error("Failed creating field $err"), - ); - }, - toggleFieldVisibility: () async { - final currentVisibility = - state.field.visibility ?? FieldVisibility.AlwaysShown; - final newVisibility = - currentVisibility == FieldVisibility.AlwaysHidden - ? FieldVisibility.AlwaysShown - : FieldVisibility.AlwaysHidden; - final result = await fieldSettingsService.updateFieldSettings( - fieldId: fieldId, - fieldVisibility: newVisibility, - ); - _logIfError(result); - }, - toggleWrapCellContent: () async { - final currentWrap = state.field.wrapCellContent ?? false; - final result = await fieldSettingsService.updateFieldSettings( - fieldId: state.field.id, - wrapCellContent: !currentWrap, - ); - _logIfError(result); - }, - ); - }, - ); - } - - void _startListening() { - _listener = (field) { - if (!isClosed) { - add(FieldEditorEvent.didUpdateField(field)); - } - }; - fieldController.addSingleFieldListener( - fieldId, - onFieldChanged: _listener, - ); - } - - void _init() async { - await Future.delayed(const Duration(milliseconds: 50)); - if (!isClosed) { - final field = fieldController.getField(fieldId); - if (field != null) { - add(FieldEditorEvent.didUpdateField(field)); - } - } - } - - void _logIfError(FlowyResult result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - } -} - -@freezed -class FieldEditorEvent with _$FieldEditorEvent { - const factory FieldEditorEvent.didUpdateField(final FieldInfo fieldInfo) = - _DidUpdateField; - const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) = - _SwitchFieldType; - const factory FieldEditorEvent.updateTypeOption( - 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() = - _ToggleFieldVisiblity; - const factory FieldEditorEvent.toggleWrapCellContent() = - _ToggleWrapCellContent; -} - -@freezed -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 deleted file mode 100644 index 46fc8659ca..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'field_info.freezed.dart'; - -@freezed -class FieldInfo with _$FieldInfo { - const FieldInfo._(); - - factory FieldInfo.initial(FieldPB field) => FieldInfo( - field: field, - fieldSettings: null, - hasFilter: false, - hasSort: false, - isGroupField: false, - ); - - const factory FieldInfo({ - required FieldPB field, - required FieldSettingsPB? fieldSettings, - required bool isGroupField, - required bool hasFilter, - required bool hasSort, - }) = _FieldInfo; - - String get id => field.id; - - FieldType get fieldType => field.fieldType; - - String get name => field.name; - - String get icon => field.icon; - - bool get isPrimary => field.isPrimary; - - double? get width => fieldSettings?.width.toDouble(); - - FieldVisibility? get visibility => fieldSettings?.visibility; - - bool? get wrapCellContent => fieldSettings?.wrapCellContent; -} 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/number_format_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/number_format_bloc.dart deleted file mode 100644 index c4c31b880e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/number_format_bloc.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'number_format_bloc.freezed.dart'; - -class NumberFormatBloc extends Bloc { - NumberFormatBloc() : super(NumberFormatState.initial()) { - on( - (event, emit) async { - event.map( - setFilter: (_SetFilter value) { - final List formats = - List.from(NumberFormatPB.values); - if (value.filter.isNotEmpty) { - formats.retainWhere( - (element) => element - .title() - .toLowerCase() - .contains(value.filter.toLowerCase()), - ); - } - emit(state.copyWith(formats: formats, filter: value.filter)); - }, - ); - }, - ); - } -} - -@freezed -class NumberFormatEvent with _$NumberFormatEvent { - const factory NumberFormatEvent.setFilter(String filter) = _SetFilter; -} - -@freezed -class NumberFormatState with _$NumberFormatState { - const factory NumberFormatState({ - required List formats, - required String filter, - }) = _NumberFormatState; - - factory NumberFormatState.initial() { - return const NumberFormatState( - formats: NumberFormatPB.values, - filter: "", - ); - } -} - -extension NumberFormatExtension on NumberFormatPB { - String title() { - switch (this) { - case NumberFormatPB.ArgentinePeso: - return "Argentine peso"; - case NumberFormatPB.Baht: - return "Baht"; - case NumberFormatPB.CanadianDollar: - return "Canadian dollar"; - case NumberFormatPB.ChileanPeso: - return "Chilean peso"; - case NumberFormatPB.ColombianPeso: - return "Colombian peso"; - case NumberFormatPB.DanishKrone: - return "Danish crown"; - case NumberFormatPB.Dirham: - return "Dirham"; - case NumberFormatPB.EUR: - return "Euro"; - case NumberFormatPB.Forint: - return "Forint"; - case NumberFormatPB.Franc: - return "Franc"; - case NumberFormatPB.HongKongDollar: - return "Hone Kong dollar"; - case NumberFormatPB.Koruna: - return "Koruna"; - case NumberFormatPB.Krona: - return "Krona"; - case NumberFormatPB.Leu: - return "Leu"; - case NumberFormatPB.Lira: - return "Lira"; - case NumberFormatPB.MexicanPeso: - return "Mexican peso"; - case NumberFormatPB.NewTaiwanDollar: - return "New Taiwan dollar"; - case NumberFormatPB.NewZealandDollar: - return "New Zealand dollar"; - case NumberFormatPB.NorwegianKrone: - return "Norwegian krone"; - case NumberFormatPB.Num: - return "Number"; - case NumberFormatPB.Percent: - return "Percent"; - case NumberFormatPB.PhilippinePeso: - return "Philippine peso"; - case NumberFormatPB.Pound: - return "Pound"; - case NumberFormatPB.Rand: - return "Rand"; - case NumberFormatPB.Real: - return "Real"; - case NumberFormatPB.Ringgit: - return "Ringgit"; - case NumberFormatPB.Riyal: - return "Riyal"; - case NumberFormatPB.Ruble: - return "Ruble"; - case NumberFormatPB.Rupee: - return "Rupee"; - case NumberFormatPB.Rupiah: - return "Rupiah"; - case NumberFormatPB.Shekel: - return "Skekel"; - case NumberFormatPB.USD: - return "US dollar"; - case NumberFormatPB.UruguayanPeso: - return "Uruguayan peso"; - case NumberFormatPB.Won: - return "Won"; - case NumberFormatPB.Yen: - return "Yen"; - case NumberFormatPB.Yuan: - return "Yuan"; - default: - throw UnimplementedError; - } - } - - String iconSymbol([bool defaultPrefixInc = true]) { - switch (this) { - case NumberFormatPB.ArgentinePeso: - return "\$"; - case NumberFormatPB.Baht: - return "฿"; - case NumberFormatPB.CanadianDollar: - return "C\$"; - case NumberFormatPB.ChileanPeso: - return "\$"; - case NumberFormatPB.ColombianPeso: - return "\$"; - case NumberFormatPB.DanishKrone: - return "kr"; - case NumberFormatPB.Dirham: - return "د.إ"; - case NumberFormatPB.EUR: - return "€"; - case NumberFormatPB.Forint: - return "Ft"; - case NumberFormatPB.Franc: - return "Fr"; - case NumberFormatPB.HongKongDollar: - return "HK\$"; - case NumberFormatPB.Koruna: - return "Kč"; - case NumberFormatPB.Krona: - return "kr"; - case NumberFormatPB.Leu: - return "lei"; - case NumberFormatPB.Lira: - return "₺"; - case NumberFormatPB.MexicanPeso: - return "\$"; - case NumberFormatPB.NewTaiwanDollar: - return "NT\$"; - case NumberFormatPB.NewZealandDollar: - return "NZ\$"; - case NumberFormatPB.NorwegianKrone: - return "kr"; - case NumberFormatPB.Num: - return defaultPrefixInc ? "#" : ""; - case NumberFormatPB.Percent: - return "%"; - case NumberFormatPB.PhilippinePeso: - return "₱"; - case NumberFormatPB.Pound: - return "£"; - case NumberFormatPB.Rand: - return "R"; - case NumberFormatPB.Real: - return "R\$"; - case NumberFormatPB.Ringgit: - return "RM"; - case NumberFormatPB.Riyal: - return "ر.س"; - case NumberFormatPB.Ruble: - return "₽"; - case NumberFormatPB.Rupee: - return "₹"; - case NumberFormatPB.Rupiah: - return "Rp"; - case NumberFormatPB.Shekel: - return "₪"; - case NumberFormatPB.USD: - return "\$"; - case NumberFormatPB.UruguayanPeso: - return "\$U"; - case NumberFormatPB.Won: - return "₩"; - case NumberFormatPB.Yen: - return "JPY ¥"; - case NumberFormatPB.Yuan: - return "¥"; - default: - throw UnimplementedError; - } - } -} 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 deleted file mode 100644 index 4ddde80b79..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'relation_type_option_cubit.freezed.dart'; - -class RelationDatabaseListCubit extends Cubit { - RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) { - _loadDatabaseMetas(); - } - - void _loadDatabaseMetas() async { - final metaPBs = await DatabaseEventGetDatabases() - .send() - .fold>((s) => s.items, (f) => []); - final futures = metaPBs.map((meta) { - return ViewBackendService.getView(meta.viewId).then( - (result) => result.fold( - (s) => DatabaseMeta( - databaseId: meta.databaseId, - viewId: meta.viewId, - databaseName: s.name, - ), - (f) => null, - ), - ); - }); - final databaseMetas = await Future.wait(futures); - emit( - RelationDatabaseListState( - databaseMetas: databaseMetas.nonNulls.toList(), - ), - ); - } -} - -@freezed -class DatabaseMeta with _$DatabaseMeta { - factory DatabaseMeta({ - /// id of the database - required String databaseId, - - /// id of the view - required String viewId, - - /// name of the database - required String databaseName, - }) = _DatabaseMeta; -} - -@freezed -class RelationDatabaseListState with _$RelationDatabaseListState { - factory RelationDatabaseListState({ - required List databaseMetas, - }) = _RelationDatabaseListState; - - factory RelationDatabaseListState.initial() => - RelationDatabaseListState(databaseMetas: []); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart deleted file mode 100644 index 72f49dd084..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'select_type_option_actions.dart'; - -part 'select_option_type_option_bloc.freezed.dart'; - -class SelectOptionTypeOptionBloc - extends Bloc { - SelectOptionTypeOptionBloc({ - required List options, - required this.typeOptionAction, - }) : super(SelectOptionTypeOptionState.initial(options)) { - _dispatch(); - } - - final ISelectOptionAction typeOptionAction; - - void _dispatch() { - on( - (event, emit) async { - event.when( - createOption: (optionName) { - final List options = - typeOptionAction.insertOption(state.options, optionName); - emit(state.copyWith(options: options)); - }, - addingOption: () { - emit(state.copyWith(isEditingOption: true, newOptionName: null)); - }, - endAddingOption: () { - emit(state.copyWith(isEditingOption: false, newOptionName: null)); - }, - updateOption: (option) { - final options = - typeOptionAction.updateOption(state.options, option); - emit(state.copyWith(options: options)); - }, - deleteOption: (option) { - final options = - typeOptionAction.deleteOption(state.options, option); - emit(state.copyWith(options: options)); - }, - reorderOption: (fromOptionId, toOptionId) { - final options = typeOptionAction.reorderOption( - state.options, - fromOptionId, - toOptionId, - ); - emit(state.copyWith(options: options)); - }, - ); - }, - ); - } -} - -@freezed -class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent { - const factory SelectOptionTypeOptionEvent.createOption(String optionName) = - _CreateOption; - const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption; - const factory SelectOptionTypeOptionEvent.endAddingOption() = - _EndAddingOption; - const factory SelectOptionTypeOptionEvent.updateOption( - SelectOptionPB option, - ) = _UpdateOption; - const factory SelectOptionTypeOptionEvent.deleteOption( - SelectOptionPB option, - ) = _DeleteOption; - const factory SelectOptionTypeOptionEvent.reorderOption( - String fromOptionId, - String toOptionId, - ) = _ReorderOption; -} - -@freezed -class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState { - const factory SelectOptionTypeOptionState({ - required List options, - required bool isEditingOption, - required String? newOptionName, - }) = _SelectOptionTypeOptionState; - - factory SelectOptionTypeOptionState.initial(List options) => - SelectOptionTypeOptionState( - options: options, - isEditingOption: false, - newOptionName: null, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart deleted file mode 100644 index 235bdb60eb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:appflowy/plugins/database/domain/type_option_service.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:nanoid/nanoid.dart'; - -abstract class ISelectOptionAction { - ISelectOptionAction({ - required this.onTypeOptionUpdated, - required String viewId, - required String fieldId, - }) : service = TypeOptionBackendService(viewId: viewId, fieldId: fieldId); - - final TypeOptionBackendService service; - final TypeOptionDataCallback onTypeOptionUpdated; - - void updateTypeOption(List options) { - final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(options); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - } - - List insertOption( - List options, - String optionName, - ) { - if (options.any((element) => element.name == optionName)) { - return options; - } - - final newOptions = List.from(options); - - final newSelectOption = SelectOptionPB() - ..id = nanoid(4) - ..color = newSelectOptionColor(options) - ..name = optionName; - - newOptions.insert(0, newSelectOption); - - updateTypeOption(newOptions); - return newOptions; - } - - List deleteOption( - List options, - SelectOptionPB deletedOption, - ) { - final newOptions = List.from(options); - final index = - newOptions.indexWhere((option) => option.id == deletedOption.id); - if (index != -1) { - newOptions.removeAt(index); - } - - updateTypeOption(newOptions); - return newOptions; - } - - List updateOption( - List options, - SelectOptionPB option, - ) { - final newOptions = List.from(options); - final index = newOptions.indexWhere((element) => element.id == option.id); - if (index != -1) { - newOptions[index] = option; - } - - updateTypeOption(newOptions); - return newOptions; - } - - List reorderOption( - List options, - String fromOptionId, - String toOptionId, - ) { - final newOptions = List.from(options); - final fromIndex = - newOptions.indexWhere((element) => element.id == fromOptionId); - final toIndex = - newOptions.indexWhere((element) => element.id == toOptionId); - - if (fromIndex != -1 && toIndex != -1) { - newOptions.insert(toIndex, newOptions.removeAt(fromIndex)); - } - - updateTypeOption(newOptions); - return newOptions; - } -} - -class MultiSelectAction extends ISelectOptionAction { - MultiSelectAction({ - required super.viewId, - required super.fieldId, - required super.onTypeOptionUpdated, - }); - - @override - void updateTypeOption(List options) { - final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(options); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - } -} - -class SingleSelectAction extends ISelectOptionAction { - SingleSelectAction({ - required super.viewId, - required super.fieldId, - required super.onTypeOptionUpdated, - }); - - @override - void updateTypeOption(List options) { - final newTypeOption = SingleSelectTypeOptionPB()..options.addAll(options); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - } -} - -SelectOptionColorPB newSelectOptionColor(List options) { - final colorFrequency = List.filled(SelectOptionColorPB.values.length, 0); - - for (final option in options) { - colorFrequency[option.color.value]++; - } - - final minIndex = colorFrequency - .asMap() - .entries - .reduce((a, b) => a.value <= b.value ? a : b) - .key; - - return SelectOptionColorPB.valueOf(minIndex) ?? SelectOptionColorPB.Purple; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/translate_type_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/translate_type_option_bloc.dart deleted file mode 100644 index 43e990f6d4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/translate_type_option_bloc.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/translate_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; - -part 'translate_type_option_bloc.freezed.dart'; - -class TranslateTypeOptionBloc - extends Bloc { - TranslateTypeOptionBloc({required TranslateTypeOptionPB option}) - : super(TranslateTypeOptionState.initial(option)) { - on( - (event, emit) async { - event.when( - selectLanguage: (languageType) { - emit( - state.copyWith( - option: _updateLanguage(languageType), - language: languageTypeToLanguage(languageType), - ), - ); - }, - ); - }, - ); - } - - TranslateTypeOptionPB _updateLanguage(TranslateLanguagePB languageType) { - state.option.freeze(); - return state.option.rebuild((option) { - option.language = languageType; - }); - } -} - -@freezed -class TranslateTypeOptionEvent with _$TranslateTypeOptionEvent { - const factory TranslateTypeOptionEvent.selectLanguage( - TranslateLanguagePB languageType, - ) = _SelectLanguage; -} - -@freezed -class TranslateTypeOptionState with _$TranslateTypeOptionState { - const factory TranslateTypeOptionState({ - required TranslateTypeOptionPB option, - required String language, - }) = _TranslateTypeOptionState; - - factory TranslateTypeOptionState.initial(TranslateTypeOptionPB option) => - TranslateTypeOptionState( - option: option, - language: languageTypeToLanguage(option.language), - ); -} - -String languageTypeToLanguage(TranslateLanguagePB langaugeType) { - switch (langaugeType) { - case TranslateLanguagePB.SimplifiedChinese: - return 'Simplified Chinese'; - case TranslateLanguagePB.TraditionalChinese: - return 'Traditional Chinese'; - case TranslateLanguagePB.English: - return 'English'; - case TranslateLanguagePB.French: - return 'French'; - case TranslateLanguagePB.German: - return 'German'; - case TranslateLanguagePB.Spanish: - return 'Spanish'; - case TranslateLanguagePB.Hindi: - return 'Hindi'; - case TranslateLanguagePB.Portuguese: - return 'Portuguese'; - case TranslateLanguagePB.StandardArabic: - return 'Standard Arabic'; - default: - Log.error('Unknown language type: $langaugeType'); - return 'English'; - } -} 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 deleted file mode 100644 index c76e6d095c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -abstract class TypeOptionParser { - T fromBuffer(List buffer); -} - -class NumberTypeOptionDataParser extends TypeOptionParser { - @override - NumberTypeOptionPB fromBuffer(List buffer) { - return NumberTypeOptionPB.fromBuffer(buffer); - } -} - -class DateTypeOptionDataParser extends TypeOptionParser { - @override - DateTypeOptionPB fromBuffer(List buffer) { - return DateTypeOptionPB.fromBuffer(buffer); - } -} - -class TimestampTypeOptionDataParser - extends TypeOptionParser { - @override - TimestampTypeOptionPB fromBuffer(List buffer) { - return TimestampTypeOptionPB.fromBuffer(buffer); - } -} - -class SingleSelectTypeOptionDataParser - extends TypeOptionParser { - @override - SingleSelectTypeOptionPB fromBuffer(List buffer) { - return SingleSelectTypeOptionPB.fromBuffer(buffer); - } -} - -class MultiSelectTypeOptionDataParser - extends TypeOptionParser { - @override - MultiSelectTypeOptionPB fromBuffer(List buffer) { - return MultiSelectTypeOptionPB.fromBuffer(buffer); - } -} - -class RelationTypeOptionDataParser - extends TypeOptionParser { - @override - RelationTypeOptionPB fromBuffer(List buffer) { - return RelationTypeOptionPB.fromBuffer(buffer); - } -} - -class TranslateTypeOptionDataParser - extends TypeOptionParser { - @override - TranslateTypeOptionPB fromBuffer(List buffer) { - 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/row/related_row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart deleted file mode 100644 index f735618dd8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart +++ /dev/null @@ -1,132 +0,0 @@ -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'; - -class RelatedRowDetailPageBloc - extends Bloc { - RelatedRowDetailPageBloc({ - required String databaseId, - required String initialRowId, - }) : super(const RelatedRowDetailPageState.loading()) { - _dispatch(); - _init(databaseId, initialRowId); - } - - UserProfilePB? _userProfile; - UserProfilePB? get userProfile => _userProfile; - - @override - Future close() { - state.whenOrNull( - ready: (databaseController, rowController) async { - await rowController.dispose(); - await databaseController.dispose(); - }, - ); - return super.close(); - } - - 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(); - emit( - RelatedRowDetailPageState.ready( - databaseController: databaseController, - rowController: rowController, - ), - ); - }, - orElse: () { - emit( - RelatedRowDetailPageState.ready( - databaseController: databaseController, - rowController: 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) { - return; - } - - final databaseView = await ViewBackendService.getView(viewId) - .fold((viewPB) => viewPB, (f) => null); - if (databaseView == null) { - return; - } - final databaseController = DatabaseController(view: databaseView); - await databaseController.open().fold( - (s) => databaseController.setIsLoading(false), - (f) => null, - ); - final rowInfo = databaseController.rowCache.getRow(initialRowId); - if (rowInfo == null) { - return; - } - final rowController = RowController( - rowMeta: rowInfo.rowMeta, - viewId: databaseView.id, - rowCache: databaseController.rowCache, - ); - - add( - RelatedRowDetailPageEvent.didInitialize( - databaseController, - rowController, - ), - ); - } -} - -@freezed -class RelatedRowDetailPageEvent with _$RelatedRowDetailPageEvent { - const factory RelatedRowDetailPageEvent.didInitialize( - DatabaseController databaseController, - RowController rowController, - ) = _DidInitialize; -} - -@freezed -class RelatedRowDetailPageState with _$RelatedRowDetailPageState { - const factory RelatedRowDetailPageState.loading() = _LoadingState; - const factory RelatedRowDetailPageState.ready({ - required DatabaseController databaseController, - required RowController rowController, - }) = _ReadyState; -} 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 deleted file mode 100644 index 7714b7727f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_banner_bloc.dart +++ /dev/null @@ -1,166 +0,0 @@ -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_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'; - -import '../../domain/row_meta_listener.dart'; - -part 'row_banner_bloc.freezed.dart'; - -class RowBannerBloc extends Bloc { - RowBannerBloc({ - required this.viewId, - required this.fieldController, - required RowMetaPB rowMeta, - }) : _rowBackendSvc = RowBackendService(viewId: viewId), - _metaListener = RowMetaListener(rowMeta.id), - super(RowBannerState.initial(rowMeta)) { - _dispatch(); - } - - final String viewId; - final FieldController fieldController; - 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(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) { - event.when( - initial: () async { - await _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), - setIcon: (String iconURL) => _updateMeta(iconURL: iconURL), - removeCover: () => _removeCover(), - didReceiveFieldUpdate: (updatedField) { - emit( - state.copyWith( - primaryField: updatedField, - loadingState: const LoadingState.finish(), - ), - ); - }, - ); - }, - ); - } - - Future _loadPrimaryField() async { - final fieldOrError = - await FieldBackendService.getPrimaryField(viewId: viewId); - fieldOrError.fold( - (primaryField) { - if (!isClosed) { - fieldController.addSingleFieldListener( - primaryField.id, - onFieldChanged: (updatedField) { - if (!isClosed) { - add(RowBannerEvent.didReceiveFieldUpdate(updatedField.field)); - } - }, - ); - add(RowBannerEvent.didReceiveFieldUpdate(primaryField)); - } - }, - (r) => Log.error(r), - ); - } - - /// Listen the changes of the row meta and then update the banner - void _listenRowMetaChanged() { - _metaListener.start( - callback: (rowMeta) { - if (!isClosed) { - add(RowBannerEvent.didReceiveRowMeta(rowMeta)); - } - }, - ); - } - - /// Update the meta of the row and the view - Future _updateMeta({String? iconURL, RowCoverPB? cover}) async { - final result = await _rowBackendSvc.updateMeta( - iconURL: iconURL, - cover: cover, - 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 -class RowBannerEvent with _$RowBannerEvent { - const factory RowBannerEvent.initial() = _Initial; - const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) = - _DidReceiveRowMeta; - 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; -} - -@freezed -class RowBannerState extends Equatable with _$RowBannerState { - const RowBannerState._(); - - const factory RowBannerState({ - required FieldPB? primaryField, - required RowMetaPB rowMeta, - required LoadingState loadingState, - }) = _RowBannerState; - - factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState( - primaryField: null, - rowMeta: rowMetaPB, - loadingState: const LoadingState.loading(), - ); - - @override - List get props => [ - rowMeta.cover.data, - rowMeta.icon, - primaryField, - loadingState, - ]; -} - -@freezed -class LoadingState with _$LoadingState { - const factory LoadingState.loading() = _Loading; - const factory LoadingState.finish() = _Finish; -} 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 deleted file mode 100644 index be5ba29dfc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart +++ /dev/null @@ -1,416 +0,0 @@ -import 'dart:collection'; - -import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart'; - -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../cell/cell_cache.dart'; -import '../cell/cell_controller.dart'; - -import 'row_list.dart'; -import 'row_service.dart'; - -part 'row_cache.freezed.dart'; - -typedef RowUpdateCallback = void Function(); - -/// A delegate that provides the fields of the row. -abstract class RowFieldsDelegate { - UnmodifiableListView get fieldInfos; - void onFieldsChanged(void Function(List) callback); -} - -abstract mixin class RowLifeCycle { - void onRowDisposed(); -} - -/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid for more information. - -class RowCache { - RowCache({ - required this.viewId, - required RowFieldsDelegate fieldsDelegate, - required RowLifeCycle rowLifeCycle, - }) : _cellMemCache = CellMemCache(), - _changedNotifier = RowChangesetNotifier(), - _rowLifeCycle = rowLifeCycle, - _fieldDelegate = fieldsDelegate { - // Listen to field changes. If a field is deleted, we can safely remove the - // cells corresponding to that field from our cache. - fieldsDelegate.onFieldsChanged((fieldInfos) { - for (final fieldInfo in fieldInfos) { - _cellMemCache.removeCellWithFieldId(fieldInfo.id); - } - - _changedNotifier?.receive(const ChangedReason.fieldDidChange()); - }); - } - - final String viewId; - final RowList _rowList = RowList(); - final CellMemCache _cellMemCache; - final RowLifeCycle _rowLifeCycle; - final RowFieldsDelegate _fieldDelegate; - RowChangesetNotifier? _changedNotifier; - bool _isInitialRows = false; - final List _pendingVisibilityChanges = []; - - /// Returns a unmodifiable list of RowInfo - UnmodifiableListView get rowInfos { - final visibleRows = [..._rowList.rows]; - return UnmodifiableListView(visibleRows); - } - - /// Returns a unmodifiable map of RowInfo - UnmodifiableMapView get rowByRowId { - return UnmodifiableMapView(_rowList.rowInfoByRowId); - } - - CellMemCache get cellCache => _cellMemCache; - ChangedReason get changeReason => - _changedNotifier?.reason ?? const InitialListState(); - - RowInfo? getRow(RowId rowId) { - return _rowList.get(rowId); - } - - void setInitialRows(List rows) { - for (final row in rows) { - 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()); - } - - void dispose() { - _rowList.dispose(); - _rowLifeCycle.onRowDisposed(); - _changedNotifier?.dispose(); - _changedNotifier = null; - _cellMemCache.dispose(); - } - - void applyRowsChanged(RowsChangePB changeset) { - _deleteRows(changeset.deletedRows); - _insertRows(changeset.insertedRows); - _updateRows(changeset.updatedRows); - } - - void applyRowsVisibility(RowsVisibilityChangePB changeset) { - if (_isInitialRows) { - _hideRows(changeset.invisibleRows); - _showRows(changeset.visibleRows); - _changedNotifier?.receive( - ChangedReason.updateRowsVisibility(changeset), - ); - } else { - _pendingVisibilityChanges.add(changeset); - } - } - - void reorderAllRows(List rowIds) { - _rowList.reorderWithRowIds(rowIds); - _changedNotifier?.receive(const ChangedReason.reorderRows()); - } - - void reorderSingleRow(ReorderSingleRowPB reorderRow) { - final rowInfo = _rowList.get(reorderRow.rowId); - if (rowInfo != null) { - _rowList.moveRow( - reorderRow.rowId, - reorderRow.oldIndex, - reorderRow.newIndex, - ); - _changedNotifier?.receive( - ChangedReason.reorderSingleRow( - reorderRow, - rowInfo, - ), - ); - } - } - - void _deleteRows(List deletedRowIds) { - for (final rowId in deletedRowIds) { - final deletedRow = _rowList.remove(rowId); - if (deletedRow != null) { - _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); - } - } - } - _changedNotifier?.receive(ChangedReason.insert(insertedIndices)); - } - - void _updateRows(List updatedRows) { - if (updatedRows.isEmpty) return; - final List updatedList = []; - for (final updatedRow in updatedRows) { - for (final fieldId in updatedRow.fieldIds) { - final key = CellContext( - fieldId: fieldId, - rowId: updatedRow.rowId, - ); - _cellMemCache.remove(key); - } - if (updatedRow.hasRowMeta()) { - updatedList.add(updatedRow.rowMeta); - } - } - - final updatedIndexs = _rowList.updateRows( - rowMetas: updatedList, - builder: (rowId) => buildGridRow(rowId), - ); - - if (updatedIndexs.isNotEmpty) { - _changedNotifier?.receive(ChangedReason.update(updatedIndexs)); - } - } - - void _hideRows(List invisibleRows) { - for (final rowId in invisibleRows) { - final deletedRow = _rowList.remove(rowId); - if (deletedRow != null) { - _changedNotifier?.receive(ChangedReason.delete(deletedRow)); - } - } - } - - void _showRows(List visibleRows) { - for (final insertedRow in visibleRows) { - final insertedIndex = - _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); - if (insertedIndex != null) { - _changedNotifier?.receive(ChangedReason.insert([insertedIndex])); - } - } - } - - void onRowsChanged(void Function(ChangedReason) onRowChanged) { - _changedNotifier?.addListener(() { - if (_changedNotifier != null) { - onRowChanged(_changedNotifier!.reason); - } - }); - } - - RowUpdateCallback addListener({ - required RowId rowId, - void Function(List, ChangedReason)? onRowChanged, - }) { - void listenerHandler() async { - if (onRowChanged != null) { - final rowInfo = _rowList.get(rowId); - if (rowInfo != null) { - final cellDataMap = _makeCells(rowInfo.rowMeta); - if (_changedNotifier != null) { - onRowChanged(cellDataMap, _changedNotifier!.reason); - } - } - } - } - - _changedNotifier?.addListener(listenerHandler); - return listenerHandler; - } - - void removeRowListener(VoidCallback callback) { - _changedNotifier?.removeListener(callback); - } - - List loadCells(RowMetaPB rowMeta) { - final rowInfo = _rowList.get(rowMeta.id); - if (rowInfo == null) { - _loadRow(rowMeta.id); - } - final cells = _makeCells(rowMeta); - return cells; - } - - Future _loadRow(RowId rowId) async { - final result = await RowBackendService.getRow(viewId: viewId, rowId: rowId); - result.fold( - (rowMetaPB) { - final rowInfo = _rowList.get(rowMetaPB.id); - final rowIndex = _rowList.indexOfRow(rowMetaPB.id); - if (rowInfo != null && rowIndex != null) { - rowInfo.rowMetaNotifier.value = rowMetaPB; - - final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); - updatedIndexs[rowMetaPB.id] = UpdatedIndex( - index: rowIndex, - rowId: rowMetaPB.id, - ); - - _changedNotifier?.receive(ChangedReason.update(updatedIndexs)); - } - }, - (err) => Log.error(err), - ); - } - - List _makeCells(RowMetaPB rowMeta) { - return _fieldDelegate.fieldInfos - .map( - (fieldInfo) => CellContext( - rowId: rowMeta.id, - fieldId: fieldInfo.id, - ), - ) - .toList(); - } - - RowInfo buildGridRow(RowMetaPB rowMetaPB) { - return RowInfo( - fields: _fieldDelegate.fieldInfos, - rowMeta: rowMetaPB, - ); - } -} - -class RowChangesetNotifier extends ChangeNotifier { - RowChangesetNotifier(); - - ChangedReason reason = const InitialListState(); - - void receive(ChangedReason newReason) { - reason = newReason; - reason.map( - insert: (_) => notifyListeners(), - delete: (_) => notifyListeners(), - update: (_) => notifyListeners(), - fieldDidChange: (_) => notifyListeners(), - initial: (_) {}, - reorderRows: (_) => notifyListeners(), - reorderSingleRow: (_) => notifyListeners(), - updateRowsVisibility: (_) => notifyListeners(), - setInitialRows: (_) => notifyListeners(), - didFetchRow: (_) => notifyListeners(), - ); - } -} - -class RowInfo extends Equatable { - RowInfo({ - required this.fields, - required RowMetaPB rowMeta, - }) : rowMetaNotifier = ValueNotifier(rowMeta), - rowIconNotifier = ValueNotifier(rowMeta.icon), - rowDocumentNotifier = ValueNotifier( - !(rowMeta.hasIsDocumentEmpty() ? rowMeta.isDocumentEmpty : true), - ); - - 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]; -} - -typedef InsertedIndexs = List; -typedef DeletedIndexs = List; -// key: id of the row -// value: UpdatedIndex -typedef UpdatedIndexMap = LinkedHashMap; - -@freezed -class ChangedReason with _$ChangedReason { - const factory ChangedReason.insert(InsertedIndexs items) = _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; -} - -class InsertedIndex { - InsertedIndex({ - required this.index, - required this.rowId, - }); - - final int index; - final RowId rowId; -} - -class DeletedIndex { - DeletedIndex({ - required this.index, - required this.rowInfo, - }); - - final int index; - final RowInfo rowInfo; -} - -class UpdatedIndex { - UpdatedIndex({ - required this.index, - required this.rowId, - }); - - final int index; - final RowId rowId; -} 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 deleted file mode 100644 index 0d2bf4985d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart +++ /dev/null @@ -1,106 +0,0 @@ -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:flutter/material.dart'; - -import '../cell/cell_cache.dart'; -import '../cell/cell_controller.dart'; -import 'row_cache.dart'; - -typedef OnRowChanged = void Function(List, ChangedReason); - -class RowController { - RowController({ - required RowMetaPB rowMeta, - required this.viewId, - required RowCache rowCache, - this.groupId, - }) : _rowMeta = rowMeta, - _rowCache = rowCache, - _rowBackendSvc = RowBackendService(viewId: viewId), - _rowListener = RowListener(rowMeta.id); - - 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); - - /// 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; - } - - 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, - }) { - final fn = _rowCache.addListener( - rowId: rowMeta.id, - onRowChanged: (context, reasons) { - if (_isDisposed) { - return; - } - onRowChanged?.call(context, reasons); - }, - ); - - // 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(); - for (final fn in _onRowChangedListeners) { - _rowCache.removeRowListener(fn); - } - } -} 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 deleted file mode 100644 index 151a32d961..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.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 '../field/field_info.dart'; - -typedef RowId = String; - -class RowBackendService { - RowBackendService({required this.viewId}); - - final String viewId; - - static Future> createRow({ - required String viewId, - String? groupId, - void Function(RowDataBuilder builder)? withCells, - OrderObjectPositionTypePB? position, - String? targetRowId, - }) { - final payload = CreateRowPayloadPB( - viewId: viewId, - groupId: groupId, - rowPosition: OrderObjectPositionPB( - position: position, - objectId: targetRowId, - ), - ); - - if (withCells != null) { - final rowBuilder = RowDataBuilder(); - withCells(rowBuilder); - payload.data.addAll(rowBuilder.build()); - } - - 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, - position: OrderObjectPositionTypePB.Before, - targetRowId: rowId, - ); - } - - Future> createRowAfter(RowId rowId) { - return createRow( - viewId: viewId, - position: OrderObjectPositionTypePB.After, - targetRowId: rowId, - ); - } - - static Future> getRow({ - required String viewId, - required String rowId, - }) { - final payload = DatabaseViewRowIdPB() - ..viewId = viewId - ..rowId = rowId; - - return DatabaseEventGetRowMeta(payload).send(); - } - - Future> getRowMeta(RowId rowId) { - final payload = DatabaseViewRowIdPB.create() - ..viewId = viewId - ..rowId = rowId; - - return DatabaseEventGetRowMeta(payload).send(); - } - - Future> updateMeta({ - required String rowId, - String? iconURL, - RowCoverPB? cover, - bool? isDocumentEmpty, - }) { - final payload = UpdateRowMetaChangesetPB.create() - ..viewId = viewId - ..id = rowId; - - if (iconURL != null) { - payload.iconUrl = iconURL; - } - if (cover != null) { - payload.cover = cover; - } - - if (isDocumentEmpty != null) { - payload.isDocumentEmpty = isDocumentEmpty; - } - - 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, - ) { - final payload = RepeatedRowIdPB.create() - ..viewId = viewId - ..rowIds.addAll(rowIds); - - return DatabaseEventDeleteRows(payload).send(); - } - - static Future> duplicateRow( - String viewId, - RowId rowId, - ) { - final payload = DatabaseViewRowIdPB( - viewId: viewId, - rowId: rowId, - ); - - return DatabaseEventDuplicateRow(payload).send(); - } -} - -class RowDataBuilder { - final _cellDataByFieldId = {}; - - void insertText(FieldInfo fieldInfo, String text) { - assert(fieldInfo.fieldType == FieldType.RichText); - _cellDataByFieldId[fieldInfo.field.id] = text; - } - - void insertNumber(FieldInfo fieldInfo, int num) { - assert(fieldInfo.fieldType == FieldType.Number); - _cellDataByFieldId[fieldInfo.field.id] = num.toString(); - } - - void insertDate(FieldInfo fieldInfo, DateTime date) { - assert(fieldInfo.fieldType == FieldType.DateTime); - final timestamp = date.millisecondsSinceEpoch ~/ 1000; - _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString(); - } - - Map build() { - return _cellDataByFieldId; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart deleted file mode 100644 index c62e30b742..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/domain/group_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'group_bloc.freezed.dart'; - -class DatabaseGroupBloc extends Bloc { - DatabaseGroupBloc({ - required String viewId, - required DatabaseController databaseController, - }) : _databaseController = databaseController, - _groupBackendSvc = GroupBackendService(viewId), - super( - DatabaseGroupState.initial( - viewId, - databaseController.fieldController.fieldInfos, - databaseController.databaseLayoutSetting!.board, - databaseController.fieldController.groupSettings, - ), - ) { - _dispatch(); - } - - final DatabaseController _databaseController; - final GroupBackendService _groupBackendSvc; - Function(List)? _onFieldsFn; - DatabaseLayoutSettingCallbacks? _layoutSettingCallbacks; - - @override - Future close() async { - if (_onFieldsFn != null) { - _databaseController.fieldController - .removeListener(onFieldsListener: _onFieldsFn!); - _onFieldsFn = null; - } - _layoutSettingCallbacks = null; - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async => _startListening(), - didReceiveFieldUpdate: (fieldInfos) { - emit( - state.copyWith( - fieldInfos: fieldInfos, - groupSettings: - _databaseController.fieldController.groupSettings, - ), - ); - }, - setGroupByField: ( - String fieldId, - FieldType fieldType, [ - List? settingContent, - ]) async { - final result = await _groupBackendSvc.groupByField( - fieldId: fieldId, - settingContent: settingContent ?? [], - ); - result.fold((l) => null, (err) => Log.error(err)); - }, - didUpdateLayoutSettings: (layoutSettings) { - emit(state.copyWith(layoutSettings: layoutSettings)); - }, - ); - }, - ); - } - - void _startListening() { - _onFieldsFn = (fieldInfos) => - add(DatabaseGroupEvent.didReceiveFieldUpdate(fieldInfos)); - _databaseController.fieldController.addListener( - onReceiveFields: _onFieldsFn, - listenWhen: () => !isClosed, - ); - - _layoutSettingCallbacks = DatabaseLayoutSettingCallbacks( - onLayoutSettingsChanged: (layoutSettings) { - if (isClosed || !layoutSettings.hasBoard()) { - return; - } - add( - DatabaseGroupEvent.didUpdateLayoutSettings(layoutSettings.board), - ); - }, - ); - _databaseController.addListener( - onLayoutSettingsChanged: _layoutSettingCallbacks, - ); - } -} - -@freezed -class DatabaseGroupEvent with _$DatabaseGroupEvent { - const factory DatabaseGroupEvent.initial() = _Initial; - const factory DatabaseGroupEvent.setGroupByField( - String fieldId, - FieldType fieldType, [ - @Default([]) List settingContent, - ]) = _DatabaseGroupEvent; - const factory DatabaseGroupEvent.didReceiveFieldUpdate( - List fields, - ) = _DidReceiveFieldUpdate; - const factory DatabaseGroupEvent.didUpdateLayoutSettings( - BoardLayoutSettingPB layoutSettings, - ) = _DidUpdateLayoutSettings; -} - -@freezed -class DatabaseGroupState with _$DatabaseGroupState { - const factory DatabaseGroupState({ - required String viewId, - required List fieldInfos, - required BoardLayoutSettingPB layoutSettings, - required List groupSettings, - }) = _DatabaseGroupState; - - factory DatabaseGroupState.initial( - String viewId, - List fieldInfos, - BoardLayoutSettingPB layoutSettings, - List groupSettings, - ) => - DatabaseGroupState( - viewId: viewId, - fieldInfos: fieldInfos, - layoutSettings: layoutSettings, - groupSettings: groupSettings, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/setting/property_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/setting/property_bloc.dart deleted file mode 100644 index 46414791f2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/setting/property_bloc.dart +++ /dev/null @@ -1,125 +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/domain/field_service.dart'; -import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'property_bloc.freezed.dart'; - -class DatabasePropertyBloc - extends Bloc { - DatabasePropertyBloc({ - required String viewId, - required FieldController fieldController, - }) : _fieldController = fieldController, - super( - DatabasePropertyState.initial( - viewId, - fieldController.fieldInfos, - ), - ) { - _dispatch(); - } - - final FieldController _fieldController; - Function(List)? _onFieldsFn; - - @override - Future close() async { - if (_onFieldsFn != null) { - _fieldController.removeListener(onFieldsListener: _onFieldsFn!); - _onFieldsFn = null; - } - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () { - _startListening(); - }, - setFieldVisibility: (fieldId, visibility) async { - final fieldSettingsSvc = - FieldSettingsBackendService(viewId: state.viewId); - - final result = await fieldSettingsSvc.updateFieldSettings( - fieldId: fieldId, - fieldVisibility: visibility, - ); - - result.fold((l) => null, (err) => Log.error(err)); - }, - didReceiveFieldUpdate: (fields) { - emit(state.copyWith(fieldContexts: fields)); - }, - moveField: (fromIndex, toIndex) async { - if (fromIndex < toIndex) { - toIndex--; - } - final fromId = state.fieldContexts[fromIndex].field.id; - final toId = state.fieldContexts[toIndex].field.id; - - final fieldContexts = List.from(state.fieldContexts); - fieldContexts.insert(toIndex, fieldContexts.removeAt(fromIndex)); - emit(state.copyWith(fieldContexts: fieldContexts)); - - final result = await FieldBackendService.moveField( - viewId: state.viewId, - fromFieldId: fromId, - toFieldId: toId, - ); - - result.fold((l) => null, (r) => Log.error(r)); - }, - ); - }, - ); - } - - void _startListening() { - _onFieldsFn = - (fields) => add(DatabasePropertyEvent.didReceiveFieldUpdate(fields)); - _fieldController.addListener( - onReceiveFields: _onFieldsFn, - listenWhen: () => !isClosed, - ); - } -} - -@freezed -class DatabasePropertyEvent with _$DatabasePropertyEvent { - const factory DatabasePropertyEvent.initial() = _Initial; - const factory DatabasePropertyEvent.setFieldVisibility( - String fieldId, - FieldVisibility visibility, - ) = _SetFieldVisibility; - const factory DatabasePropertyEvent.didReceiveFieldUpdate( - List fields, - ) = _DidReceiveFieldUpdate; - const factory DatabasePropertyEvent.moveField(int fromIndex, int toIndex) = - _MoveField; -} - -@freezed -class DatabasePropertyState with _$DatabasePropertyState { - const factory DatabasePropertyState({ - required String viewId, - required List fieldContexts, - }) = _GridPropertyState; - - factory DatabasePropertyState.initial( - String viewId, - List fieldContexts, - ) => - DatabasePropertyState( - viewId: viewId, - fieldContexts: fieldContexts, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/share_bloc.dart deleted file mode 100644 index 1beaaedbfb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/share_bloc.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/workspace/application/settings/share/export_service.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 'share_bloc.freezed.dart'; - -class DatabaseShareBloc extends Bloc { - DatabaseShareBloc({ - required this.view, - }) : super(const DatabaseShareState.initial()) { - on(_onShareCSV); - } - - final ViewPB view; - - Future _onShareCSV( - ShareCSV event, - Emitter emit, - ) async { - emit(const DatabaseShareState.loading()); - - final result = await BackendExportService.exportDatabaseAsCSV(view.id); - result.fold( - (l) => _saveCSVToPath(l.data, event.path), - (r) => Log.error(r), - ); - - emit( - DatabaseShareState.finish( - result.fold( - (l) { - _saveCSVToPath(l.data, event.path); - return FlowyResult.success(null); - }, - (r) => FlowyResult.failure(r), - ), - ), - ); - } - - ExportDataPB _saveCSVToPath(String markdown, String path) { - File(path).writeAsStringSync(markdown); - return ExportDataPB() - ..data = markdown - ..exportType = ExportType.Markdown; - } -} - -@freezed -class DatabaseShareEvent with _$DatabaseShareEvent { - const factory DatabaseShareEvent.shareCSV(String path) = ShareCSV; -} - -@freezed -class DatabaseShareState with _$DatabaseShareState { - const factory DatabaseShareState.initial() = _Initial; - const factory DatabaseShareState.loading() = _Loading; - const factory DatabaseShareState.finish( - FlowyResult successOrFail, - ) = _Finish; -} 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 deleted file mode 100644 index 351dea2cd8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/sync/database_sync_state_listener.dart'; -import 'package:appflowy/plugins/database/domain/database_view_service.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'database_sync_bloc.freezed.dart'; - -class DatabaseSyncBloc extends Bloc { - DatabaseSyncBloc({ - required this.view, - }) : super(DatabaseSyncBlocState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - final userProfile = await getIt().getUser().then( - (value) => value.fold((s) => s, (f) => null), - ); - final databaseId = await DatabaseViewBackendService(viewId: view.id) - .getDatabaseId() - .then((value) => value.fold((s) => s, (f) => null)); - emit( - state.copyWith( - shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server && - databaseId != null, - ), - ); - if (databaseId != null) { - _syncStateListener = - DatabaseSyncStateListener(databaseId: databaseId) - ..start( - didReceiveSyncState: (syncState) { - Log.info( - 'database sync state changed, from ${state.syncState} to $syncState', - ); - add(DatabaseSyncEvent.syncStateChanged(syncState)); - }, - ); - } - - final isNetworkConnected = await _connectivity - .checkConnectivity() - .then((value) => value != ConnectivityResult.none); - emit(state.copyWith(isNetworkConnected: isNetworkConnected)); - - connectivityStream = - _connectivity.onConnectivityChanged.listen((result) { - add(DatabaseSyncEvent.networkStateChanged(result)); - }); - }, - syncStateChanged: (syncState) { - emit(state.copyWith(syncState: syncState.value)); - }, - networkStateChanged: (result) { - emit( - state.copyWith( - isNetworkConnected: result != ConnectivityResult.none, - ), - ); - }, - ); - }, - ); - } - - final ViewPB view; - final _connectivity = Connectivity(); - - StreamSubscription? connectivityStream; - DatabaseSyncStateListener? _syncStateListener; - - @override - Future close() async { - await connectivityStream?.cancel(); - await _syncStateListener?.stop(); - return super.close(); - } -} - -@freezed -class DatabaseSyncEvent with _$DatabaseSyncEvent { - const factory DatabaseSyncEvent.initial() = Initial; - const factory DatabaseSyncEvent.syncStateChanged( - DatabaseSyncStatePB syncState, - ) = syncStateChanged; - const factory DatabaseSyncEvent.networkStateChanged( - ConnectivityResult result, - ) = NetworkStateChanged; -} - -@freezed -class DatabaseSyncBlocState with _$DatabaseSyncBlocState { - const factory DatabaseSyncBlocState({ - required DatabaseSyncState syncState, - @Default(true) bool isNetworkConnected, - @Default(false) bool shouldShowIndicator, - }) = _DatabaseSyncState; - - factory DatabaseSyncBlocState.initial() => const DatabaseSyncBlocState( - syncState: DatabaseSyncState.Syncing, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart deleted file mode 100644 index 67914e3007..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.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-notification/subject.pb.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -typedef DatabaseSyncStateCallback = void Function( - DatabaseSyncStatePB syncState, -); - -class DatabaseSyncStateListener { - DatabaseSyncStateListener({ - // NOTE: NOT the view id. - required this.databaseId, - }); - - final String databaseId; - StreamSubscription? _subscription; - DatabaseNotificationParser? _parser; - - DatabaseSyncStateCallback? didReceiveSyncState; - - void start({ - DatabaseSyncStateCallback? didReceiveSyncState, - }) { - this.didReceiveSyncState = didReceiveSyncState; - - _parser = DatabaseNotificationParser( - id: databaseId, - callback: _callback, - ); - _subscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - void _callback( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateDatabaseSyncUpdate: - result.map( - (r) { - final value = DatabaseSyncStatePB.fromBuffer(r); - didReceiveSyncState?.call(value); - }, - ); - break; - default: - break; - } - } - - Future stop() async { - await _subscription?.cancel(); - _subscription = 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 deleted file mode 100644 index e55bbb96a4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart +++ /dev/null @@ -1,340 +0,0 @@ -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: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'; - -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, - ), - ) { - on( - (event, emit) async { - await event.when( - initial: () { - _listenInlineViewChanged(); - _loadChildView(); - }, - didLoadChildViews: (List childViews) { - emit( - state.copyWith( - tabBars: [ - ...state.tabBars, - ...childViews.map( - (newChildView) => DatabaseTabBar(view: newChildView), - ), - ], - tabBarControllerByViewId: _extendsTabBarController(childViews), - ), - ); - }, - selectView: (String viewId) { - final index = - state.tabBars.indexWhere((element) => element.viewId == viewId); - if (index != -1) { - emit( - state.copyWith(selectedIndex: index), - ); - } - }, - createView: (layout, name) { - _createLinkedView(layout.layoutType, name ?? layout.layoutName); - }, - deleteView: (String viewId) async { - final result = await ViewBackendService.deleteView(viewId: viewId); - result.fold( - (l) {}, - (r) => Log.error(r), - ); - }, - renameView: (String viewId, String newName) { - ViewBackendService.updateView(viewId: viewId, name: newName); - }, - didUpdateChildViews: (updatePB) async { - if (updatePB.createChildViews.isNotEmpty) { - final allTabBars = [ - ...state.tabBars, - ...updatePB.createChildViews - .map((e) => DatabaseTabBar(view: e)), - ]; - emit( - state.copyWith( - tabBars: allTabBars, - selectedIndex: state.tabBars.length, - tabBarControllerByViewId: - _extendsTabBarController(updatePB.createChildViews), - ), - ); - } - - if (updatePB.deleteChildViews.isNotEmpty) { - final allTabBars = [...state.tabBars]; - final tabBarControllerByViewId = { - ...state.tabBarControllerByViewId, - }; - var newSelectedIndex = state.selectedIndex; - for (final viewId in updatePB.deleteChildViews) { - final index = allTabBars.indexWhere( - (element) => element.viewId == viewId, - ); - if (index != -1) { - final tabBar = allTabBars.removeAt(index); - // Dispose the controller when the tab is removed. - final controller = - tabBarControllerByViewId.remove(tabBar.viewId); - await controller?.dispose(); - } - - if (index == state.selectedIndex) { - if (index > 0 && allTabBars.isNotEmpty) { - newSelectedIndex = index - 1; - } - } - } - emit( - state.copyWith( - tabBars: allTabBars, - selectedIndex: newSelectedIndex, - tabBarControllerByViewId: tabBarControllerByViewId, - ), - ); - } - }, - viewDidUpdate: (ViewPB updatedView) { - final index = state.tabBars.indexWhere( - (element) => element.viewId == updatedView.id, - ); - if (index != -1) { - final allTabBars = [...state.tabBars]; - final updatedTabBar = DatabaseTabBar(view: updatedView); - allTabBars[index] = updatedTabBar; - emit(state.copyWith(tabBars: allTabBars)); - } - }, - ); - }, - ); - } - - @override - Future close() async { - for (final tabBar in state.tabBars) { - await state.tabBarControllerByViewId[tabBar.viewId]?.dispose(); - tabBar.dispose(); - } - return super.close(); - } - - void _listenInlineViewChanged() { - final controller = state.tabBarControllerByViewId[state.parentView.id]; - controller?.onViewUpdated = (newView) { - add(DatabaseTabBarEvent.viewDidUpdate(newView)); - }; - - // Only listen the child view changes when the parent view is inline. - controller?.onViewChildViewChanged = (update) { - add(DatabaseTabBarEvent.didUpdateChildViews(update)); - }; - } - - /// Create tab bar controllers for the new views and return the updated map. - Map _extendsTabBarController( - List newViews, - ) { - 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)); - }; - - tabBarControllerByViewId[view.id] = controller; - } - return tabBarControllerByViewId; - } - - Future _createLinkedView(ViewLayoutPB layoutType, String name) async { - final viewId = state.parentView.id; - final databaseIdOrError = - await DatabaseViewBackendService(viewId: viewId).getDatabaseId(); - databaseIdOrError.fold( - (databaseId) async { - final linkedViewOrError = - await ViewBackendService.createDatabaseLinkedView( - parentViewId: viewId, - databaseId: databaseId, - layoutType: layoutType, - name: name, - ); - - linkedViewOrError.fold( - (linkedView) {}, - (err) => Log.error(err), - ); - }, - (r) => Log.error(r), - ); - } - - void _loadChildView() async { - final viewsOrFail = - await ViewBackendService.getChildViews(viewId: state.parentView.id); - - viewsOrFail.fold( - (views) { - if (!isClosed) { - add(DatabaseTabBarEvent.didLoadChildViews(views)); - } - }, - (err) => Log.error(err), - ); - } -} - -@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; -} - -@freezed -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, - ) { - 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, - ), - }, - ); - } -} - -class DatabaseTabBar extends Equatable { - DatabaseTabBar({ - required this.view, - }) : _builder = UniversalPlatform.isMobile - ? view.mobileTabBarItem() - : view.tabBarItem(); - - final ViewPB view; - final DatabaseTabBarItemBuilder _builder; - - String get viewId => view.id; - - DatabaseTabBarItemBuilder get builder => _builder; - - ViewLayoutPB get layout => view.layout; - - @override - List get props => [view.hashCode]; - - void dispose() { - _builder.dispose(); - } -} - -typedef OnViewUpdated = void Function(ViewPB newView); -typedef OnViewChildViewChanged = void Function( - ChildViewUpdatePB childViewUpdate, -); - -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)); - }, - ), - viewListener = ViewListener(viewId: view.id) { - viewListener.start( - onViewChildViewsUpdated: (update) => onViewChildViewChanged?.call(update), - onViewUpdated: (newView) { - view = newView; - onViewUpdated?.call(newView); - }, - ); - } - - ViewPB view; - final DatabaseController controller; - final ViewListener viewListener; - OnViewUpdated? onViewUpdated; - OnViewChildViewChanged? onViewChildViewChanged; - - Future dispose() async { - await Future.wait([viewListener.stop(), 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 deleted file mode 100644 index 754b2d1c23..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy_backend/log.dart'; - -import '../defines.dart'; -import '../field/field_controller.dart'; -import '../row/row_cache.dart'; - -import 'view_listener.dart'; - -class DatabaseViewCallbacks { - const DatabaseViewCallbacks({ - this.onNumOfRowsChanged, - this.onRowsCreated, - this.onRowsUpdated, - this.onRowsDeleted, - }); - - /// Will get called when number of rows were changed that includes - /// update/delete/insert rows. The [onNumOfRowsChanged] will return all - /// the rows of the current database - final OnNumOfRowsChanged? onNumOfRowsChanged; - - // Will get called when creating new rows - final OnRowsCreated? onRowsCreated; - - /// Will get called when rows were updated - final OnRowsUpdated? onRowsUpdated; - - /// Will get called when number of rows were deleted - final OnRowsDeleted? onRowsDeleted; -} - -/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid for more information -class DatabaseViewCache { - DatabaseViewCache({ - required this.viewId, - required FieldController fieldController, - }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) { - final depsImpl = RowCacheDependenciesImpl(fieldController); - _rowCache = RowCache( - viewId: viewId, - fieldsDelegate: depsImpl, - rowLifeCycle: depsImpl, - ); - - _databaseViewListener.start( - onRowsChanged: (result) => result.fold( - (changeset) { - // Update the cache - _rowCache.applyRowsChanged(changeset); - - if (changeset.deletedRows.isNotEmpty) { - for (final callback in _callbacks) { - callback.onRowsDeleted?.call(changeset.deletedRows); - } - } - - if (changeset.updatedRows.isNotEmpty) { - for (final callback in _callbacks) { - callback.onRowsUpdated?.call( - changeset.updatedRows.map((e) => e.rowId).toList(), - _rowCache.changeReason, - ); - } - } - - if (changeset.insertedRows.isNotEmpty) { - for (final callback in _callbacks) { - callback.onRowsCreated?.call(changeset.insertedRows); - } - } - }, - (err) => Log.error(err), - ), - onRowsVisibilityChanged: (result) => result.fold( - (changeset) => _rowCache.applyRowsVisibility(changeset), - (err) => Log.error(err), - ), - onReorderAllRows: (result) => result.fold( - (rowIds) => _rowCache.reorderAllRows(rowIds), - (err) => Log.error(err), - ), - onReorderSingleRow: (result) => result.fold( - (reorderRow) => _rowCache.reorderSingleRow(reorderRow), - (err) => Log.error(err), - ), - ); - - _rowCache.onRowsChanged( - (reason) { - for (final callback in _callbacks) { - callback.onNumOfRowsChanged - ?.call(rowInfos, _rowCache.rowByRowId, reason); - } - }, - ); - } - - final String viewId; - late RowCache _rowCache; - final DatabaseViewListener _databaseViewListener; - final List _callbacks = []; - - UnmodifiableListView get rowInfos => _rowCache.rowInfos; - RowCache get rowCache => _rowCache; - - RowInfo? getRow(RowId rowId) => _rowCache.getRow(rowId); - - Future dispose() async { - await _databaseViewListener.stop(); - _rowCache.dispose(); - _callbacks.clear(); - } - - void addListener(DatabaseViewCallbacks callbacks) { - _callbacks.add(callbacks); - } -} 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 deleted file mode 100644 index 3f97304296..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; -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'; - -typedef RowsVisibilityCallback = void Function( - FlowyResult, -); -typedef NumberOfRowsCallback = void Function( - FlowyResult, -); -typedef ReorderAllRowsCallback = void Function( - FlowyResult, FlowyError>, -); -typedef SingleRowCallback = void Function( - FlowyResult, -); - -class DatabaseViewListener { - DatabaseViewListener({required this.viewId}); - - final String viewId; - DatabaseNotificationListener? _listener; - - void start({ - required NumberOfRowsCallback onRowsChanged, - required ReorderAllRowsCallback onReorderAllRows, - required SingleRowCallback onReorderSingleRow, - required RowsVisibilityCallback onRowsVisibilityChanged, - }) { - // Stop any existing listener - _listener?.stop(); - - // Initialize the notification listener - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: (ty, result) => _handler( - ty, - result, - onRowsChanged, - onReorderAllRows, - onReorderSingleRow, - onRowsVisibilityChanged, - ), - ); - } - - 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)), - ); - break; - case DatabaseNotification.DidUpdateRow: - result.fold( - (payload) => onRowsChanged( - FlowyResult.success(RowsChangePB.fromBuffer(payload)), - ), - (error) => onRowsChanged(FlowyResult.failure(error)), - ); - break; - case DatabaseNotification.DidReorderRows: - result.fold( - (payload) => onReorderAllRows( - FlowyResult.success(ReorderAllRowsPB.fromBuffer(payload).rowOrders), - ), - (error) => onReorderAllRows(FlowyResult.failure(error)), - ); - break; - case DatabaseNotification.DidReorderSingleRow: - result.fold( - (payload) => onReorderSingleRow( - FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)), - ), - (error) => onReorderSingleRow(FlowyResult.failure(error)), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _listener = 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 deleted file mode 100644 index 12a1603430..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_actions_bloc.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'board_actions_bloc.freezed.dart'; - -class BoardActionsCubit extends Cubit { - BoardActionsCubit({ - required this.databaseController, - }) : super(const BoardActionsState.initial()); - - final DatabaseController databaseController; - - void startEditingRow(GroupedRowId groupedRowId) { - emit(BoardActionsState.startEditingRow(groupedRowId: groupedRowId)); - emit(const BoardActionsState.initial()); - } - - void endEditing(GroupedRowId groupedRowId) { - emit(const BoardActionsState.endEditingRow()); - emit(BoardActionsState.setFocus(groupedRowIds: [groupedRowId])); - emit(const BoardActionsState.initial()); - } - - void openCard(RowMetaPB rowMeta) { - emit(BoardActionsState.openCard(rowMeta: rowMeta)); - emit(const BoardActionsState.initial()); - } - - void openCardWithRowId(rowId) { - final rowMeta = databaseController.rowCache.getRow(rowId)!.rowMeta; - openCard(rowMeta); - } - - void setFocus(List groupedRowIds) { - emit(BoardActionsState.setFocus(groupedRowIds: groupedRowIds)); - emit(const BoardActionsState.initial()); - } - - void startCreateBottomRow(String groupId) { - emit(const BoardActionsState.setFocus(groupedRowIds: [])); - emit(BoardActionsState.startCreateBottomRow(groupId: groupId)); - emit(const BoardActionsState.initial()); - } - - void createRow( - GroupedRowId? groupedRowId, - CreateBoardCardRelativePosition relativePosition, - ) { - emit( - BoardActionsState.createRow( - groupedRowId: groupedRowId, - position: relativePosition, - ), - ); - emit(const BoardActionsState.initial()); - } -} - -@freezed -class BoardActionsState with _$BoardActionsState { - const factory BoardActionsState.initial() = _BoardActionsInitialState; - - const factory BoardActionsState.openCard({ - required RowMetaPB rowMeta, - }) = _BoardActionsOpenCardState; - - const factory BoardActionsState.startEditingRow({ - required GroupedRowId groupedRowId, - }) = _BoardActionsStartEditingRowState; - - const factory BoardActionsState.endEditingRow() = - _BoardActionsEndEditingRowState; - - const factory BoardActionsState.setFocus({ - required List groupedRowIds, - }) = _BoardActionsSetFocusState; - - const factory BoardActionsState.startCreateBottomRow({ - required String groupId, - }) = _BoardActionsStartCreateBottomRowState; - - const factory BoardActionsState.createRow({ - required GroupedRowId? groupedRowId, - required CreateBoardCardRelativePosition position, - }) = _BoardActionCreateRowState; -} - -enum CreateBoardCardRelativePosition { - before, - after, -} 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 deleted file mode 100644 index a2c6c89578..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart +++ /dev/null @@ -1,812 +0,0 @@ -import 'dart:async'; -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_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_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.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' hide FieldInfo; -import 'package:universal_platform/universal_platform.dart'; - -import '../../application/database_controller.dart'; -import '../../application/field/field_controller.dart'; -import '../../application/row/row_cache.dart'; -import 'group_controller.dart'; - -part 'board_bloc.freezed.dart'; - -class BoardBloc extends Bloc { - BoardBloc({ - required this.databaseController, - this.didCreateRow, - AppFlowyBoardController? boardController, - }) : super(const BoardState.loading()) { - groupBackendSvc = GroupBackendService(viewId); - _initBoardController(boardController); - _dispatch(); - } - - final DatabaseController databaseController; - late final AppFlowyBoardController boardController; - final LinkedHashMap groupControllers = - LinkedHashMap(); - final List groupList = []; - - final ValueNotifier? didCreateRow; - - 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( - onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) => - databaseController.moveGroup( - fromGroupId: fromGroupId, - toGroupId: toGroupId, - ), - onMoveGroupItem: (groupId, fromIndex, toIndex) { - final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex); - final toRow = groupControllers[groupId]?.rowAtIndex(toIndex); - if (fromRow != null) { - databaseController.moveGroupRow( - fromRow: fromRow, - toRow: toRow, - fromGroupId: groupId, - toGroupId: groupId, - ); - } - }, - onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) { - final fromRow = - groupControllers[fromGroupId]?.rowAtIndex(fromIndex); - final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex); - if (fromRow != null) { - databaseController.moveGroupRow( - fromRow: fromRow, - toRow: toRow, - fromGroupId: fromGroupId, - toGroupId: toGroupId, - ); - } - }, - ); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - 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 - .firstWhereOrNull((element) => element.isPrimary)!; - final void Function(RowDataBuilder)? cellBuilder = title == null - ? null - : (builder) => builder.insertText(primaryField, title); - - final result = await RowBackendService.createRow( - viewId: databaseController.viewId, - groupId: groupId, - position: position, - targetRowId: targetRowId, - withCells: cellBuilder, - ); - - final startEditing = position != OrderObjectPositionTypePB.End; - final action = UniversalPlatform.isMobile - ? DidCreateRowAction.openAsPage - : startEditing - ? DidCreateRowAction.startEditing - : DidCreateRowAction.none; - - result.fold( - (rowMeta) { - state.maybeMap( - ready: (value) { - didCreateRow?.value = DidCreateRowResult( - action: action, - rowMeta: rowMeta, - groupId: groupId, - ); - }, - orElse: () {}, - ); - }, - (err) => Log.error(err), - ); - }, - createGroup: (name) async { - final result = await groupBackendSvc.createGroup(name: name); - result.onFailure(Log.error); - }, - 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); - }, - didReceiveError: (error) { - emit(BoardState.error(error: error)); - }, - didReceiveGroups: (List groups) { - state.maybeMap( - ready: (state) { - emit( - state.copyWith( - hiddenGroups: _filterHiddenGroups(hideUngrouped, groups), - groupIds: groups.map((group) => group.groupId).toList(), - ), - ); - }, - orElse: () {}, - ); - }, - didUpdateLayoutSettings: (layoutSettings) { - state.maybeMap( - ready: (state) { - emit( - state.copyWith( - layoutSettings: layoutSettings, - hiddenGroups: _filterHiddenGroups(hideUngrouped, groupList), - ), - ); - }, - orElse: () {}, - ); - }, - setGroupVisibility: (GroupPB group, bool isVisible) async { - await _setGroupVisibility(group, isVisible); - }, - toggleHiddenSectionVisibility: (isVisible) async { - await state.maybeMap( - ready: (state) async { - final newLayoutSettings = state.layoutSettings!; - newLayoutSettings.freeze(); - - final newLayoutSetting = newLayoutSettings.rebuild( - (message) => message.collapseHiddenGroups = isVisible, - ); - - await databaseController.updateLayoutSetting( - boardLayoutSetting: newLayoutSetting, - ); - }, - orElse: () {}, - ); - }, - reorderGroup: (fromGroupId, toGroupId) async { - _reorderGroup(fromGroupId, toGroupId, emit); - }, - startEditingHeader: (String groupId) { - state.maybeMap( - ready: (state) => emit(state.copyWith(editingHeaderId: groupId)), - orElse: () {}, - ); - }, - 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, - ); - } - } - - state.maybeMap( - ready: (state) => emit(state.copyWith(editingHeaderId: null)), - orElse: () {}, - ); - }, - deleteCards: (groupedRowIds) async { - final rowIds = groupedRowIds.map((e) => e.rowId).toList(); - await RowBackendService.deleteRows(viewId, rowIds); - }, - moveGroupToAdjacentGroup: (groupedRowId, toPrevious) async { - final fromRow = - databaseController.rowCache.getRow(groupedRowId.rowId)?.rowMeta; - final currentGroupIndex = - boardController.groupIds.indexOf(groupedRowId.groupId); - final toGroupIndex = - toPrevious ? currentGroupIndex - 1 : currentGroupIndex + 1; - if (fromRow != null && - toGroupIndex > -1 && - toGroupIndex < boardController.groupIds.length) { - final toGroupId = boardController.groupDatas[toGroupIndex].id; - final result = await databaseController.moveGroupRow( - fromRow: fromRow, - fromGroupId: groupedRowId.groupId, - toGroupId: toGroupId, - ); - result.fold( - (s) { - final previousState = state; - emit( - BoardState.setFocus( - groupedRowIds: [ - GroupedRowId( - groupId: toGroupId, - rowId: groupedRowId.rowId, - ), - ], - ), - ); - emit(previousState); - }, - (f) {}, - ); - } - }, - openRowDetail: (rowMeta) { - final copyState = state; - emit(BoardState.openRowDetail(rowMeta: rowMeta)); - emit(copyState); - }, - ); - }, - ); - } - - Future _setGroupVisibility(GroupPB group, bool isVisible) async { - if (group.isDefault) { - await state.maybeMap( - ready: (state) async { - final newLayoutSettings = state.layoutSettings!; - newLayoutSettings.freeze(); - - final newLayoutSetting = newLayoutSettings.rebuild( - (message) => message.hideUngroupedColumn = !isVisible, - ); - - await databaseController.updateLayoutSetting( - boardLayoutSetting: newLayoutSetting, - ); - }, - orElse: () {}, - ); - } else { - await groupBackendSvc.updateGroup( - groupId: group.groupId, - visible: isVisible, - ); - } - } - - void _reorderGroup( - String fromGroupId, - String toGroupId, - Emitter emit, - ) async { - final fromIndex = groupList.indexWhere((g) => g.groupId == fromGroupId); - final toIndex = groupList.indexWhere((g) => g.groupId == toGroupId); - final group = groupList.removeAt(fromIndex); - groupList.insert(toIndex, group); - add(BoardEvent.didReceiveGroups(groupList)); - final result = await databaseController.moveGroup( - fromGroupId: fromGroupId, - toGroupId: toGroupId, - ); - result.fold((l) => {}, (err) => Log.error(err)); - } - - @override - Future close() async { - 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(); - } - - bool get hideUngrouped => - databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ?? - false; - - FieldType? get groupingFieldType { - if (groupList.isEmpty) { - return null; - } - return databaseController.fieldController - .getField(groupList.first.fieldId) - ?.fieldType; - } - - void initializeGroups(List groups) { - for (final controller in groupControllers.values) { - controller.dispose(); - } - - groupControllers.clear(); - boardController.clear(); - groupList.clear(); - groupList.addAll(groups); - - boardController.addGroups( - groups - .where((group) { - final field = fieldController.getField(group.fieldId); - return field != null && - (!group.isDefault && group.isVisible || - group.isDefault && - !hideUngrouped && - field.fieldType != FieldType.Checkbox); - }) - .map((group) => _initializeGroupData(group)) - .toList(), - ); - - for (final group in groups) { - final controller = _initializeGroupController(group); - groupControllers[controller.group.groupId] = controller; - } - } - - RowCache get rowCache => databaseController.rowCache; - - void _startListening() { - _layoutSettingsCallback = DatabaseLayoutSettingCallbacks( - onLayoutSettingsChanged: (layoutSettings) { - if (isClosed) { - return; - } - final index = groupList.indexWhere((element) => element.isDefault); - if (index != -1) { - if (layoutSettings.board.hideUngroupedColumn) { - boardController.removeGroup(groupList[index].fieldId); - } else { - final newGroup = _initializeGroupData(groupList[index]); - final visibleGroups = [...groupList] - ..retainWhere((g) => g.isVisible || g.isDefault); - final indexInVisibleGroups = - visibleGroups.indexWhere((g) => g.isDefault); - if (indexInVisibleGroups != -1) { - boardController.insertGroup(indexInVisibleGroups, newGroup); - } - } - } - add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board)); - }, - ); - _groupCallbacks = GroupCallbacks( - onGroupByField: (groups) { - if (isClosed) { - return; - } - - initializeGroups(groups); - add(BoardEvent.didReceiveGroups(groups)); - }, - onDeleteGroup: (groupIds) { - if (isClosed) { - return; - } - - boardController.removeGroups(groupIds); - groupList.removeWhere((group) => groupIds.contains(group.groupId)); - add(BoardEvent.didReceiveGroups(groupList)); - }, - onInsertGroup: (insertGroups) { - if (isClosed) { - return; - } - - final group = insertGroups.group; - final newGroup = _initializeGroupData(group); - final controller = _initializeGroupController(group); - groupControllers[controller.group.groupId] = controller; - boardController.addGroup(newGroup); - groupList.insert(insertGroups.index, group); - add(BoardEvent.didReceiveGroups(groupList)); - }, - onUpdateGroup: (updatedGroups) async { - if (isClosed) { - return; - } - - // workaround: update group most of the time gets called before fields in - // field controller are updated. For single and multi-select group - // renames, this is required before generating the new group name. - await Future.delayed(const Duration(milliseconds: 50)); - - for (final group in updatedGroups) { - // see if the column is already in the board - final index = groupList.indexWhere((g) => g.groupId == group.groupId); - if (index == -1) { - continue; - } - - final columnController = - boardController.getGroupController(group.groupId); - if (columnController != null) { - // remove the group or update its name - columnController.updateGroupName( - group.generateGroupName(databaseController), - ); - if (!group.isVisible) { - boardController.removeGroup(group.groupId); - } - } else { - final newGroup = _initializeGroupData(group); - final visibleGroups = [...groupList]..retainWhere( - (g) => - (g.isVisible && !g.isDefault) || - g.isDefault && !hideUngrouped || - g.groupId == group.groupId, - ); - final indexInVisibleGroups = - visibleGroups.indexWhere((g) => g.groupId == group.groupId); - if (indexInVisibleGroups != -1) { - boardController.insertGroup(indexInVisibleGroups, newGroup); - } - } - - groupList.removeAt(index); - groupList.insert(index, group); - } - 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, - ); - } - - List _buildGroupItems(GroupPB group) { - final items = group.rows.map((row) { - final fieldInfo = fieldController.getField(group.fieldId); - return GroupItem( - row: row, - fieldInfo: fieldInfo!, - ); - }).toList(); - - return [...items]; - } - - Future _openDatabase(Emitter emit) { - return databaseController.open().fold( - (datbasePB) => databaseController.setIsLoading(false), - (err) => emit(BoardState.error(error: err)), - ); - } - - GroupController _initializeGroupController(GroupPB group) { - group.freeze(); - - final delegate = GroupControllerDelegateImpl( - controller: boardController, - fieldController: fieldController, - onNewColumnItem: (groupId, row, index) {}, - ); - - final controller = GroupController( - group: group, - delegate: delegate, - onGroupChanged: (newGroup) { - if (isClosed) return; - - final index = - groupList.indexWhere((g) => g.groupId == newGroup.groupId); - if (index != -1) { - groupList.removeAt(index); - groupList.insert(index, newGroup); - add(BoardEvent.didReceiveGroups(groupList)); - } - }, - ); - - return controller..startListening(); - } - - AppFlowyGroupData _initializeGroupData(GroupPB group) { - return AppFlowyGroupData( - id: group.groupId, - name: group.generateGroupName(databaseController), - items: _buildGroupItems(group), - customData: GroupData( - group: group, - fieldInfo: fieldController.getField(group.fieldId)!, - ), - ); - } -} - -@freezed -class BoardEvent with _$BoardEvent { - const factory BoardEvent.initial() = _InitialBoard; - const factory BoardEvent.createRow( - String groupId, - OrderObjectPositionTypePB position, - String? title, - String? targetRowId, - ) = _CreateRow; - const factory BoardEvent.createGroup(String name) = _CreateGroup; - const factory BoardEvent.startEditingHeader(String groupId) = - _StartEditingHeader; - const factory BoardEvent.endEditingHeader(String groupId, String? groupName) = - _EndEditingHeader; - const factory BoardEvent.setGroupVisibility( - GroupPB group, - bool isVisible, - ) = _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; - const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; - const factory BoardEvent.didReceiveGroups(List groups) = - _DidReceiveGroups; - const factory BoardEvent.didUpdateLayoutSettings( - BoardLayoutSettingPB layoutSettings, - ) = _DidUpdateLayoutSettings; - const factory BoardEvent.deleteCards(List groupedRowIds) = - _DeleteCards; - const factory BoardEvent.moveGroupToAdjacentGroup( - GroupedRowId groupedRowId, - bool toPrevious, - ) = _MoveGroupToAdjacentGroup; - const factory BoardEvent.openRowDetail(RowMetaPB rowMeta) = _OpenRowDetail; -} - -@freezed -class BoardState with _$BoardState { - const BoardState._(); - - const factory BoardState.loading() = _BoardLoadingState; - - const factory BoardState.error({ - required FlowyError error, - }) = _BoardErrorState; - - const factory BoardState.ready({ - required String viewId, - required List groupIds, - required LoadingState loadingState, - required FlowyError? noneOrError, - required BoardLayoutSettingPB? layoutSettings, - required List hiddenGroups, - String? editingHeaderId, - }) = _BoardReadyState; - - const factory BoardState.setFocus({ - required List groupedRowIds, - }) = _BoardSetFocusState; - - const factory BoardState.openRowDetail({ - required RowMetaPB rowMeta, - }) = _BoardOpenRowDetailState; - - factory BoardState.initial(String viewId) => BoardState.ready( - viewId: viewId, - groupIds: [], - noneOrError: null, - loadingState: const LoadingState.loading(), - layoutSettings: null, - hiddenGroups: [], - ); - - bool get isLoading => maybeMap(loading: (_) => true, orElse: () => false); - bool get isError => maybeMap(error: (_) => true, orElse: () => false); - bool get isReady => maybeMap(ready: (_) => true, orElse: () => false); - bool get isSetFocus => maybeMap(setFocus: (_) => true, orElse: () => false); -} - -List _filterHiddenGroups(bool hideUngrouped, List groups) { - return [...groups]..retainWhere( - (group) => !group.isVisible || group.isDefault && hideUngrouped, - ); -} - -class GroupItem extends AppFlowyGroupItem { - GroupItem({ - required this.row, - required this.fieldInfo, - }); - - final RowMetaPB row; - final FieldInfo fieldInfo; - - @override - String get id => row.id.toString(); -} - -/// Identifies a card in a database view that has grouping. To support cases -/// in which a card can belong to more than one group at the same time (e.g. -/// FieldType.Multiselect), we include the card's group id as well. -/// -class GroupedRowId extends Equatable { - const GroupedRowId({ - required this.rowId, - required this.groupId, - }); - - final String rowId; - final String groupId; - - @override - List get props => [rowId, groupId]; -} - -class GroupControllerDelegateImpl extends GroupControllerDelegate { - GroupControllerDelegateImpl({ - required this.controller, - required this.fieldController, - required this.onNewColumnItem, - }); - - final FieldController fieldController; - final AppFlowyBoardController controller; - final void Function(String, RowMetaPB, int?) onNewColumnItem; - - @override - bool hasGroup(String groupId) => controller.groupIds.contains(groupId); - - @override - void insertRow(GroupPB group, RowMetaPB row, int? index) { - final fieldInfo = fieldController.getField(group.fieldId); - if (fieldInfo == null) { - return Log.warn("fieldInfo should not be null"); - } - - if (index != null) { - final item = GroupItem( - row: row, - fieldInfo: fieldInfo, - ); - controller.insertGroupItem(group.groupId, index, item); - } else { - final item = GroupItem( - row: row, - fieldInfo: fieldInfo, - ); - controller.addGroupItem(group.groupId, item); - } - } - - @override - void removeRow(GroupPB group, RowId rowId) => - controller.removeGroupItem(group.groupId, rowId.toString()); - - @override - void updateRow(GroupPB group, RowMetaPB row) { - final fieldInfo = fieldController.getField(group.fieldId); - if (fieldInfo == null) { - return Log.warn("fieldInfo should not be null"); - } - - controller.updateGroupItem( - group.groupId, - GroupItem( - row: row, - fieldInfo: fieldInfo, - ), - ); - } - - @override - void addNewRow(GroupPB group, RowMetaPB row, int? index) { - final fieldInfo = fieldController.getField(group.fieldId); - if (fieldInfo == null) { - return Log.warn("fieldInfo should not be null"); - } - - final item = GroupItem(row: row, fieldInfo: fieldInfo); - - if (index != null) { - controller.insertGroupItem(group.groupId, index, item); - } else { - controller.addGroupItem(group.groupId, item); - } - - onNewColumnItem(group.groupId, row, index); - } -} - -class GroupData { - const GroupData({ - required this.group, - required this.fieldInfo, - }); - - final GroupPB group; - final FieldInfo fieldInfo; - - CheckboxGroup? asCheckboxGroup() => - fieldType == FieldType.Checkbox ? CheckboxGroup(group) : null; - - FieldType get fieldType => fieldInfo.fieldType; -} - -class CheckboxGroup { - const CheckboxGroup(this.group); - - final GroupPB group; - - // Hardcode value: "Yes" that equal to the value defined in Rust - // pub const CHECK: &str = "Yes"; - bool get isCheck => group.groupId == "Yes"; -} - -enum DidCreateRowAction { - none, - openAsPage, - startEditing, -} - -class DidCreateRowResult { - DidCreateRowResult({ - required this.action, - required this.rowMeta, - required this.groupId, - }); - - final DidCreateRowAction action; - final RowMetaPB rowMeta; - final String groupId; -} 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 deleted file mode 100644 index 4e0ad9bada..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.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_result/appflowy_result.dart'; -import 'package:flowy_infra/notifier.dart'; -import 'package:protobuf/protobuf.dart'; - -abstract class GroupControllerDelegate { - bool hasGroup(String groupId); - void removeRow(GroupPB group, RowId rowId); - void insertRow(GroupPB group, RowMetaPB row, int? index); - void updateRow(GroupPB group, RowMetaPB row); - void addNewRow(GroupPB group, RowMetaPB row, int? index); -} - -class GroupController { - GroupController({ - required this.group, - required this.delegate, - required this.onGroupChanged, - }) : _listener = SingleGroupListener(group); - - GroupPB group; - final SingleGroupListener _listener; - final GroupControllerDelegate delegate; - final void Function(GroupPB group) onGroupChanged; - - RowMetaPB? rowAtIndex(int index) => group.rows.elementAtOrNull(index); - - RowMetaPB? firstRow() => group.rows.firstOrNull; - - RowMetaPB? lastRow() => group.rows.lastOrNull; - - void startListening() { - _listener.start( - onGroupChanged: (result) { - result.fold( - (GroupRowsNotificationPB changeset) { - final newItems = [...group.rows]; - final isGroupExist = delegate.hasGroup(group.groupId); - for (final deletedRow in changeset.deletedRows) { - newItems.removeWhere((rowPB) => rowPB.id == deletedRow); - if (isGroupExist) { - delegate.removeRow(group, deletedRow); - } - } - - 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) { - newItems.insert(insertedRow.index, insertedRow.rowMeta); - } else { - newItems.add(insertedRow.rowMeta); - } - - if (isGroupExist) { - if (insertedRow.isNew) { - delegate.addNewRow(group, insertedRow.rowMeta, index); - } else { - delegate.insertRow(group, insertedRow.rowMeta, index); - } - } - } - - for (final updatedRow in changeset.updatedRows) { - final index = newItems.indexWhere( - (rowPB) => rowPB.id == updatedRow.id, - ); - - if (index != -1) { - newItems[index] = updatedRow; - if (isGroupExist) { - delegate.updateRow(group, updatedRow); - } - } - } - - 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), - ); - }, - ); - } - - Future dispose() async { - await _listener.stop(); - } -} - -typedef UpdateGroupNotifiedValue - = FlowyResult; - -class SingleGroupListener { - SingleGroupListener(this.group); - - final GroupPB group; - - PublishNotifier? _groupNotifier = PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(UpdateGroupNotifiedValue) onGroupChanged, - }) { - _groupNotifier?.addPublishListener(onGroupChanged); - _listener = DatabaseNotificationListener( - objectId: group.groupId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateGroupRow: - result.fold( - (payload) => _groupNotifier?.value = - FlowyResult.success(GroupRowsNotificationPB.fromBuffer(payload)), - (error) => _groupNotifier?.value = FlowyResult.failure(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _groupNotifier?.dispose(); - _groupNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/board.dart b/frontend/appflowy_flutter/lib/plugins/database/board/board.dart deleted file mode 100644 index f8fb18a561..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/board.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class BoardPluginBuilder implements PluginBuilder { - @override - Plugin build(dynamic data) { - if (data is ViewPB) { - return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); - } else { - throw FlowyPluginException.invalidData; - } - } - - @override - String get menuName => LocaleKeys.board_menuName.tr(); - - @override - FlowySvgData get icon => FlowySvgs.icon_board_s; - - @override - PluginType get pluginType => PluginType.board; - - @override - ViewLayoutPB get layoutType => ViewLayoutPB.Board; -} - -class BoardPluginConfig implements PluginConfig { - @override - bool get creatable => true; -} 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 deleted file mode 100644 index 70d00bcd25..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ /dev/null @@ -1,917 +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/database/board/mobile_board_page.dart'; -import 'package:appflowy/plugins/database/application/database_controller.dart'; -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/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: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' 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'; -import '../application/board_bloc.dart'; -import 'toolbar/board_setting_bar.dart'; -import 'widgets/board_focus_scope.dart'; -import 'widgets/board_hidden_groups.dart'; -import 'widgets/board_shortcut_container.dart'; - -class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { - final _toggleExtension = ToggleExtensionNotifier(); - - @override - Widget content( - BuildContext context, - ViewPB view, - DatabaseController controller, - bool shrinkWrap, - String? initialRowId, - ) => - UniversalPlatform.isDesktop - ? DesktopBoardPage( - key: _makeValueKey(controller), - view: view, - databaseController: controller, - shrinkWrap: shrinkWrap, - ) - : MobileBoardPage( - key: _makeValueKey(controller), - view: view, - databaseController: controller, - ); - - @override - Widget settingBar(BuildContext context, DatabaseController controller) => - BoardSettingBar( - key: _makeValueKey(controller), - databaseController: controller, - toggleExtension: _toggleExtension, - ); - - @override - Widget settingBarExtension( - BuildContext context, - DatabaseController controller, - ) { - return DatabaseViewSettingExtension( - key: _makeValueKey(controller), - viewId: controller.viewId, - databaseController: controller, - toggleExtension: _toggleExtension, - ); - } - - @override - void dispose() { - _toggleExtension.dispose(); - super.dispose(); - } - - ValueKey _makeValueKey(DatabaseController controller) => - ValueKey(controller.viewId); -} - -class DesktopBoardPage extends StatefulWidget { - const DesktopBoardPage({ - super.key, - required this.view, - required this.databaseController, - this.onEditStateChanged, - this.shrinkWrap = false, - }); - - final ViewPB view; - - final DatabaseController databaseController; - - /// Called when edit state changed - final VoidCallback? onEditStateChanged; - - /// If true, the board will shrink wrap its content - final bool shrinkWrap; - - @override - State createState() => _DesktopBoardPageState(); -} - -class _DesktopBoardPageState extends State { - late final AppFlowyBoardController _boardController = AppFlowyBoardController( - onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) => - widget.databaseController.moveGroup( - fromGroupId: fromGroupId, - toGroupId: toGroupId, - ), - onMoveGroupItem: (groupId, fromIndex, toIndex) { - final groupControllers = _boardBloc.groupControllers; - final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex); - final toRow = groupControllers[groupId]?.rowAtIndex(toIndex); - if (fromRow != null) { - widget.databaseController.moveGroupRow( - fromRow: fromRow, - toRow: toRow, - fromGroupId: groupId, - toGroupId: groupId, - ); - } - }, - onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) { - final groupControllers = _boardBloc.groupControllers; - final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex); - final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex); - if (fromRow != null) { - widget.databaseController.moveGroupRow( - fromRow: fromRow, - toRow: toRow, - fromGroupId: fromGroupId, - toGroupId: toGroupId, - ); - } - }, - onStartDraggingCard: (groupId, index) { - final groupControllers = _boardBloc.groupControllers; - final toRow = groupControllers[groupId]?.rowAtIndex(index); - if (toRow != null) { - _focusScope.clear(); - } - }, - ); - - late final _focusScope = BoardFocusScope( - boardController: _boardController, - ); - late final BoardBloc _boardBloc; - late final BoardActionsCubit _boardActionsCubit; - late final ValueNotifier _didCreateRow; - - @override - void initState() { - super.initState(); - _didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow); - _boardBloc = BoardBloc( - databaseController: widget.databaseController, - didCreateRow: _didCreateRow, - boardController: _boardController, - )..add(const BoardEvent.initial()); - _boardActionsCubit = BoardActionsCubit( - databaseController: widget.databaseController, - ); - } - - @override - void dispose() { - _focusScope.dispose(); - _boardBloc.close(); - _boardActionsCubit.close(); - _didCreateRow.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - 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)), - orElse: () => _BoardContent( - shrinkWrap: widget.shrinkWrap, - onEditStateChanged: widget.onEditStateChanged, - focusScope: _focusScope, - boardController: _boardController, - view: widget.view, - ), - ), - ), - ); - } - - void _handleDidCreateRow() async { - // work around: wait for the new card to be inserted into the board before enabling edit - await Future.delayed(const Duration(milliseconds: 50)); - if (_didCreateRow.value != null) { - final result = _didCreateRow.value!; - switch (result.action) { - case DidCreateRowAction.openAsPage: - _boardActionsCubit.openCard(result.rowMeta); - break; - case DidCreateRowAction.startEditing: - _boardActionsCubit.startEditingRow( - GroupedRowId( - groupId: result.groupId, - rowId: result.rowMeta.id, - ), - ); - break; - default: - break; - } - } - } -} - -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(); -} - -class _BoardContentState extends State<_BoardContent> { - final ScrollController scrollController = ScrollController(); - final AppFlowyBoardScrollController scrollManager = - AppFlowyBoardScrollController(); - - final config = const AppFlowyBoardConfig( - groupMargin: EdgeInsets.symmetric(horizontal: 4), - groupBodyPadding: EdgeInsets.symmetric(horizontal: 4), - groupFooterPadding: EdgeInsets.fromLTRB(8, 14, 8, 4), - groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8), - cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3), - stretchGroupHeight: false, - ); - - late final cellBuilder = CardCellBuilder( - databaseController: databaseController, - ); - - DatabaseController get databaseController => - context.read().databaseController; - - @override - void dispose() { - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final horizontalPadding = - context.read()?.horizontalPadding ?? - 0.0; - return MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) { - state.maybeMap( - ready: (value) { - widget.onEditStateChanged?.call(); - }, - openRowDetail: (value) { - _openCard( - context: context, - databaseController: - context.read().databaseController, - rowMeta: value.rowMeta, - ); - }, - orElse: () {}, - ); - }, - ), - BlocListener( - listener: (context, state) { - state.maybeMap( - openCard: (value) { - _openCard( - context: context, - databaseController: - context.read().databaseController, - rowMeta: value.rowMeta, - ); - }, - setFocus: (value) { - widget.focusScope.focusedGroupedRows = value.groupedRowIds; - }, - startEditingRow: (value) { - widget.boardController.enableGroupDragging(false); - widget.focusScope.clear(); - }, - endEditingRow: (value) { - widget.boardController.enableGroupDragging(true); - }, - orElse: () {}, - ); - }, - ), - ], - child: FocusScope( - autofocus: true, - child: BoardShortcutContainer( - 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( - 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, - ), - ), - 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, - ), - ), - ); - }, - ), - ), - ); - }, - ), - ), - ), - ), - ); - } -} - -@visibleForTesting -class BoardColumnFooter extends StatefulWidget { - const BoardColumnFooter({ - super.key, - required this.columnData, - required this.boardConfig, - required this.scrollManager, - }); - - final AppFlowyGroupData columnData; - final AppFlowyBoardConfig boardConfig; - final AppFlowyBoardScrollController scrollManager; - - @override - State createState() => _BoardColumnFooterState(); -} - -class _BoardColumnFooterState extends State { - final TextEditingController _textController = TextEditingController(); - late final FocusNode _focusNode; - bool _isCreating = false; - - @override - void initState() { - super.initState(); - _focusNode = FocusNode( - onKeyEvent: (node, event) { - if (_focusNode.hasFocus && - event.logicalKey == LogicalKeyboardKey.escape) { - _focusNode.unfocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - )..addListener(() { - if (!_focusNode.hasFocus) { - setState(() => _isCreating = false); - } - }); - } - - @override - void dispose() { - _textController.dispose(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isCreating) { - _focusNode.requestFocus(); - } - }); - return Padding( - padding: widget.boardConfig.groupFooterPadding, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 150), - child: - _isCreating ? _createCardsTextField() : _startCreatingCardsButton(), - ), - ); - } - - Widget _createCardsTextField() { - const nada = DoNothingAndStopPropagationIntent(); - return Shortcuts( - shortcuts: { - const SingleActivator(LogicalKeyboardKey.arrowUp): nada, - const SingleActivator(LogicalKeyboardKey.arrowDown): nada, - const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada, - const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): nada, - const SingleActivator(LogicalKeyboardKey.keyE): nada, - const SingleActivator(LogicalKeyboardKey.keyN): nada, - const SingleActivator(LogicalKeyboardKey.delete): nada, - // const SingleActivator(LogicalKeyboardKey.backspace): nada, - const SingleActivator(LogicalKeyboardKey.enter): nada, - const SingleActivator(LogicalKeyboardKey.numpadEnter): nada, - const SingleActivator(LogicalKeyboardKey.comma): nada, - const SingleActivator(LogicalKeyboardKey.period): nada, - SingleActivator( - LogicalKeyboardKey.arrowUp, - shift: true, - meta: Platform.isMacOS, - control: !Platform.isMacOS, - ): nada, - }, - child: FlowyTextField( - hintTextConstraints: const BoxConstraints(maxHeight: 36), - controller: _textController, - focusNode: _focusNode, - onSubmitted: (name) { - context.read().add( - BoardEvent.createRow( - widget.columnData.id, - OrderObjectPositionTypePB.End, - name, - null, - ), - ); - widget.scrollManager.scrollToBottom(widget.columnData.id); - _textController.clear(); - _focusNode.requestFocus(); - }, - ), - ); - } - - Widget _startCreatingCardsButton() { - return BlocListener( - listener: (context, state) { - state.maybeWhen( - startCreateBottomRow: (groupId) { - if (groupId == widget.columnData.id) { - setState(() => _isCreating = true); - } - }, - orElse: () {}, - ); - }, - child: FlowyTooltip( - message: LocaleKeys.board_column_addToColumnBottomTooltip.tr(), - child: SizedBox( - height: 36, - child: FlowyButton( - leftIcon: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).hintColor, - ), - text: FlowyText( - LocaleKeys.board_column_createNewCard.tr(), - color: Theme.of(context).hintColor, - ), - onTap: () { - context - .read() - .startCreateBottomRow(widget.columnData.id); - }, - ), - ), - ), - ); - } -} - -class _BoardCard extends StatefulWidget { - const _BoardCard({ - required this.afGroupData, - required this.groupItem, - required this.boardConfig, - required this.cellBuilder, - required this.notifier, - required this.compactMode, - required this.onOpenCard, - }); - - final AppFlowyGroupData afGroupData; - final GroupItem groupItem; - final AppFlowyBoardConfig boardConfig; - final CardCellBuilder cellBuilder; - final BoardFocusScope notifier; - final bool compactMode; - final void Function(RowMetaPB) onOpenCard; - - @override - State<_BoardCard> createState() => _BoardCardState(); -} - -class _BoardCardState extends State<_BoardCard> { - bool _isEditing = false; - - @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; - - const nada = DoNothingAndStopPropagationIntent(); - - return BlocListener( - listener: (context, state) { - state.maybeMap( - startEditingRow: (value) { - if (value.groupedRowId.rowId == widget.groupItem.id && - value.groupedRowId.groupId == groupData.group.groupId) { - setState(() => _isEditing = true); - } - }, - endEditingRow: (_) { - if (_isEditing) { - setState(() => _isEditing = false); - } - }, - createRow: (value) { - if ((_isEditing && value.groupedRowId == null) || - (value.groupedRowId?.rowId == widget.groupItem.id && - value.groupedRowId?.groupId == groupData.group.groupId)) { - context.read().add( - BoardEvent.createRow( - groupData.group.groupId, - value.position == CreateBoardCardRelativePosition.before - ? OrderObjectPositionTypePB.Before - : OrderObjectPositionTypePB.After, - null, - widget.groupItem.row.id, - ), - ); - } - }, - orElse: () {}, - ); - }, - child: Shortcuts( - shortcuts: { - const SingleActivator(LogicalKeyboardKey.arrowUp): nada, - const SingleActivator(LogicalKeyboardKey.arrowDown): nada, - const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada, - const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): - nada, - const SingleActivator(LogicalKeyboardKey.keyE): nada, - const SingleActivator(LogicalKeyboardKey.keyN): nada, - const SingleActivator(LogicalKeyboardKey.delete): nada, - // const SingleActivator(LogicalKeyboardKey.backspace): nada, - const SingleActivator(LogicalKeyboardKey.enter): nada, - const SingleActivator(LogicalKeyboardKey.numpadEnter): nada, - const SingleActivator(LogicalKeyboardKey.comma): nada, - const SingleActivator(LogicalKeyboardKey.period): nada, - SingleActivator( - LogicalKeyboardKey.arrowUp, - shift: true, - meta: Platform.isMacOS, - control: !Platform.isMacOS, - ): nada, - }, - child: ConditionalListenableBuilder>( - valueListenable: widget.notifier, - buildWhen: (previous, current) { - final focusItem = GroupedRowId( - groupId: groupData.group.groupId, - rowId: rowMeta.id, - ); - final previousContainsFocus = previous.contains(focusItem); - final currentContainsFocus = current.contains(focusItem); - - 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, - ); - }, - child: RowCard( - fieldController: databaseController.fieldController, - rowMeta: rowMeta, - viewId: boardBloc.viewId, - rowCache: rowCache, - groupingFieldId: widget.groupItem.fieldInfo.id, - isEditing: _isEditing, - cellBuilder: widget.cellBuilder, - onTap: (context) => widget.onOpenCard( - context.read().rowController.rowMeta, - ), - onShiftTap: (_) { - Focus.of(context).requestFocus(); - widget.notifier.toggle( - GroupedRowId( - rowId: widget.groupItem.row.id, - groupId: groupData.group.groupId, - ), - ); - }, - styleConfiguration: RowCardStyleConfiguration( - cellStyleMap: desktopBoardCardCellStyleMap(context), - hoverStyle: HoverStyle( - hoverColor: Theme.of(context).brightness == Brightness.light - ? const Color(0x0F1F2329) - : const Color(0x0FEFF4FB), - foregroundColorOnHover: - AFThemeExtension.of(context).onBackground, - ), - ), - onStartEditing: () => - context.read().startEditingRow( - GroupedRowId( - groupId: groupData.group.groupId, - rowId: rowMeta.id, - ), - ), - onEndEditing: () => context.read().endEditing( - GroupedRowId( - groupId: groupData.group.groupId, - rowId: rowMeta.id, - ), - ), - userProfile: context.read().userProfile, - ), - ), - ), - ); - } - - BoxDecoration _makeBoxDecoration( - BuildContext context, - String groupId, - String rowId, - ) { - return BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(6)), - border: Border.fromBorderSide( - BorderSide( - color: widget.notifier - .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(0xFF59647A), - ), - ), - boxShadow: [ - BoxShadow( - blurRadius: 4, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - ), - BoxShadow( - blurRadius: 4, - spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - ), - ], - ); - } -} - -class BoardTrailing extends StatefulWidget { - const BoardTrailing({super.key, required this.scrollController}); - - final ScrollController scrollController; - - @override - State createState() => _BoardTrailingState(); -} - -class _BoardTrailingState extends State { - final TextEditingController _textController = TextEditingController(); - late final FocusNode _focusNode; - - bool isEditing = false; - - void _cancelAddNewGroup() { - _textController.clear(); - setState(() => isEditing = false); - } - - @override - void initState() { - super.initState(); - _focusNode = FocusNode( - onKeyEvent: (node, event) { - if (_focusNode.hasFocus && - event.logicalKey == LogicalKeyboardKey.escape) { - _cancelAddNewGroup(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - )..addListener(_onFocusChanged); - } - - @override - void dispose() { - _focusNode.removeListener(_onFocusChanged); - _focusNode.dispose(); - _textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // call after every setState - WidgetsBinding.instance.addPostFrameCallback((_) { - if (isEditing) { - _focusNode.requestFocus(); - widget.scrollController.jumpTo( - widget.scrollController.position.maxScrollExtent, - ); - } - }); - - return Container( - padding: const EdgeInsets.only(left: 8.0, top: 12, right: 40), - alignment: AlignmentDirectional.topStart, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: isEditing - ? SizedBox( - width: 256, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _textController, - focusNode: _focusNode, - decoration: InputDecoration( - suffixIcon: Padding( - padding: const EdgeInsets.only(left: 4, bottom: 8.0), - child: FlowyIconButton( - icon: const FlowySvg(FlowySvgs.close_filled_s), - hoverColor: Colors.transparent, - onPressed: () => _textController.clear(), - ), - ), - suffixIconConstraints: - BoxConstraints.loose(const Size(20, 24)), - border: const UnderlineInputBorder(), - contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 8), - isDense: true, - ), - style: Theme.of(context).textTheme.bodySmall, - onSubmitted: (groupName) => context - .read() - .add(BoardEvent.createGroup(groupName)), - ), - ), - ) - : FlowyTooltip( - message: LocaleKeys.board_column_createNewColumn.tr(), - child: FlowyIconButton( - width: 26, - icon: const FlowySvg(FlowySvgs.add_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - onPressed: () => setState(() => isEditing = true), - ), - ), - ), - ); - } - - void _onFocusChanged() { - if (!_focusNode.hasFocus) { - _cancelAddNewGroup(); - } - } -} - -void _openCard({ - required BuildContext context, - required DatabaseController databaseController, - required RowMetaPB rowMeta, -}) { - final rowController = RowController( - rowMeta: rowMeta, - viewId: databaseController.viewId, - rowCache: databaseController.rowCache, - ); - - FlowyOverlay.show( - context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: RowDetailPage( - databaseController: databaseController, - rowController: rowController, - userProfile: context.read().userProfile, - ), - ), - ); -} 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 deleted file mode 100644 index e57364b2d8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart +++ /dev/null @@ -1,61 +0,0 @@ -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 BoardSettingBar extends StatelessWidget { - const BoardSettingBar({ - 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, - ), - ], - ), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index abd28ac022..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart +++ /dev/null @@ -1,250 +0,0 @@ -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/workspace/presentation/widgets/dialogs.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 '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; - - @override - State createState() => _BoardColumnHeaderState(); -} - -class _BoardColumnHeaderState extends State { - final ValueNotifier isEditing = ValueNotifier(false); - - GroupData get customData => widget.groupData.customData; - - @override - void dispose() { - isEditing.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, - ), - }; - - return Container( - padding: widget.margin, - height: 50, - child: child, - ); - } -} - -class GroupOptionsButton extends StatelessWidget { - const GroupOptionsButton({ - super.key, - required this.groupData, - this.isEditing, - }); - - final AppFlowyGroupData groupData; - final ValueNotifier? isEditing; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - clickHandler: PopoverClickHandler.gestureDetector, - margin: const EdgeInsets.all(8), - constraints: BoxConstraints.loose(const Size(168, 300)), - direction: PopoverDirection.bottomWithLeftAligned, - child: FlowyIconButton( - width: 20, - icon: const FlowySvg(FlowySvgs.details_horizontal_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - ), - popupBuilder: (popoverContext) { - final customGroupData = groupData.customData as GroupData; - final isDefault = customGroupData.group.isDefault; - final menuItems = GroupOption.values.toList(); - if (!customGroupData.fieldType.canEditHeader || isDefault) { - menuItems.remove(GroupOption.rename); - } - if (!customGroupData.fieldType.canDeleteGroup || isDefault) { - menuItems.remove(GroupOption.delete); - } - return SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4), - children: [ - ...menuItems.map( - (action) => SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - leftIcon: FlowySvg(action.icon), - text: FlowyText( - action.text, - lineHeight: 1.0, - overflow: TextOverflow.ellipsis, - ), - onTap: () { - run(context, action, customGroupData.group); - PopoverContainer.of(popoverContext).close(); - }, - ), - ), - ), - ], - ); - }, - ); - } - - void run(BuildContext context, GroupOption option, GroupPB group) { - switch (option) { - case GroupOption.rename: - isEditing?.value = true; - break; - case GroupOption.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: () { - context - .read() - .add(BoardEvent.deleteGroup(group.groupId)); - }, - ); - 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, - hide => FlowySvgs.hide_s, - delete => FlowySvgs.delete_s, - }; - - String get text => switch (this) { - rename => LocaleKeys.board_column_renameColumn.tr(), - hide => LocaleKeys.board_column_hideColumn.tr(), - delete => LocaleKeys.board_column_deleteColumn.tr(), - }; -} 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_focus_scope.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_focus_scope.dart deleted file mode 100644 index ed329904b9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_focus_scope.dart +++ /dev/null @@ -1,373 +0,0 @@ -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy_board/appflowy_board.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; - -class BoardFocusScope extends ChangeNotifier - implements ValueListenable> { - BoardFocusScope({ - required this.boardController, - }); - - final AppFlowyBoardController boardController; - List _focusedCards = []; - - @override - List get value => _focusedCards; - - UnmodifiableListView get focusedGroupedRows => - UnmodifiableListView(_focusedCards); - - set focusedGroupedRows(List focusedGroupedRows) { - _deepCopy(); - _focusedCards - ..clear() - ..addAll(focusedGroupedRows); - notifyListeners(); - } - - bool isFocused(GroupedRowId groupedRowId) => - _focusedCards.contains(groupedRowId); - - void toggle(GroupedRowId groupedRowId) { - _deepCopy(); - if (_focusedCards.contains(groupedRowId)) { - _focusedCards.remove(groupedRowId); - } else { - _focusedCards.add(groupedRowId); - } - notifyListeners(); - } - - bool focusNext() { - _deepCopy(); - - // if no card is focused, focus on the first card in the board - if (_focusedCards.isEmpty) { - _focusFirstCard(); - notifyListeners(); - return true; - } - - final lastFocusedCard = _focusedCards.last; - final groupController = boardController.controller(lastFocusedCard.groupId); - final iterable = groupController?.items - .skipWhile((item) => item.id != lastFocusedCard.rowId); - - // if the last-focused card's group cannot be found, or if the last-focused card cannot be found in the group, focus on the first card in the board - if (iterable == null || iterable.isEmpty) { - _focusFirstCard(); - notifyListeners(); - return true; - } - - if (iterable.length == 1) { - // focus on the first card in the next group - final group = boardController.groupDatas - .skipWhile((item) => item.id != lastFocusedCard.groupId) - .skip(1) - .firstWhereOrNull((groupData) => groupData.items.isNotEmpty); - if (group != null) { - _focusedCards - ..clear() - ..add( - GroupedRowId( - rowId: group.items.first.id, - groupId: group.id, - ), - ); - } - } else { - // focus on the next card in the same group - _focusedCards - ..clear() - ..add( - GroupedRowId( - rowId: iterable.elementAt(1).id, - groupId: lastFocusedCard.groupId, - ), - ); - } - - notifyListeners(); - - return true; - } - - bool focusPrevious() { - _deepCopy(); - - // if no card is focused, focus on the last card in the board - if (_focusedCards.isEmpty) { - _focusLastCard(); - notifyListeners(); - return true; - } - - final lastFocusedCard = _focusedCards.last; - final groupController = boardController.controller(lastFocusedCard.groupId); - final iterable = groupController?.items.reversed - .skipWhile((item) => item.id != lastFocusedCard.rowId); - - // if the last-focused card's group cannot be found or if the last-focused card cannot be found in the group, focus on the last card in the board - if (iterable == null || iterable.isEmpty) { - _focusLastCard(); - notifyListeners(); - return true; - } - - if (iterable.length == 1) { - // focus on the last card in the previous group - final group = boardController.groupDatas.reversed - .skipWhile((item) => item.id != lastFocusedCard.groupId) - .skip(1) - .firstWhereOrNull((groupData) => groupData.items.isNotEmpty); - if (group != null) { - _focusedCards - ..clear() - ..add( - GroupedRowId( - rowId: group.items.last.id, - groupId: group.id, - ), - ); - } - } else { - // focus on the next card in the same group - _focusedCards - ..clear() - ..add( - GroupedRowId( - rowId: iterable.elementAt(1).id, - groupId: lastFocusedCard.groupId, - ), - ); - } - - notifyListeners(); - - return true; - } - - bool adjustRangeDown() { - _deepCopy(); - - // if no card is focused, focus on the first card in the board - if (_focusedCards.isEmpty) { - _focusFirstCard(); - notifyListeners(); - return true; - } - - final firstFocusedCard = _focusedCards.first; - final lastFocusedCard = _focusedCards.last; - - // determine whether to shrink or expand the selection - bool isExpand = false; - if (_focusedCards.length == 1) { - isExpand = true; - } else { - final firstGroupIndex = boardController.groupDatas - .indexWhere((element) => element.id == firstFocusedCard.groupId); - final lastGroupIndex = boardController.groupDatas - .indexWhere((element) => element.id == lastFocusedCard.groupId); - - if (firstGroupIndex == -1 || lastGroupIndex == -1) { - _focusFirstCard(); - notifyListeners(); - return true; - } - - if (firstGroupIndex < lastGroupIndex) { - isExpand = true; - } else if (firstGroupIndex > lastGroupIndex) { - isExpand = false; - } else { - final groupItems = - boardController.groupDatas.elementAt(firstGroupIndex).items; - final firstCardIndex = - groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId); - final lastCardIndex = - groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId); - - if (firstCardIndex == -1 || lastCardIndex == -1) { - _focusFirstCard(); - notifyListeners(); - return true; - } - - isExpand = firstCardIndex < lastCardIndex; - } - } - - if (isExpand) { - final groupController = - boardController.controller(lastFocusedCard.groupId); - - if (groupController == null) { - _focusFirstCard(); - notifyListeners(); - return true; - } - - final iterable = groupController.items - .skipWhile((item) => item.id != lastFocusedCard.rowId); - - if (iterable.length == 1) { - // focus on the first card in the next group - final group = boardController.groupDatas - .skipWhile((item) => item.id != lastFocusedCard.groupId) - .skip(1) - .firstWhereOrNull((groupData) => groupData.items.isNotEmpty); - if (group != null) { - _focusedCards.add( - GroupedRowId( - rowId: group.items.first.id, - groupId: group.id, - ), - ); - } - } else { - _focusedCards.add( - GroupedRowId( - rowId: iterable.elementAt(1).id, - groupId: lastFocusedCard.groupId, - ), - ); - } - } else { - _focusedCards.removeLast(); - } - - notifyListeners(); - return true; - } - - bool adjustRangeUp() { - _deepCopy(); - - // if no card is focused, focus on the first card in the board - if (_focusedCards.isEmpty) { - _focusLastCard(); - notifyListeners(); - return true; - } - - final firstFocusedCard = _focusedCards.first; - final lastFocusedCard = _focusedCards.last; - - // determine whether to shrink or expand the selection - bool isExpand = false; - if (_focusedCards.length == 1) { - isExpand = true; - } else { - final firstGroupIndex = boardController.groupDatas - .indexWhere((element) => element.id == firstFocusedCard.groupId); - final lastGroupIndex = boardController.groupDatas - .indexWhere((element) => element.id == lastFocusedCard.groupId); - - if (firstGroupIndex == -1 || lastGroupIndex == -1) { - _focusLastCard(); - notifyListeners(); - return true; - } - - if (firstGroupIndex < lastGroupIndex) { - isExpand = false; - } else if (firstGroupIndex > lastGroupIndex) { - isExpand = true; - } else { - final groupItems = - boardController.groupDatas.elementAt(firstGroupIndex).items; - final firstCardIndex = - groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId); - final lastCardIndex = - groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId); - - if (firstCardIndex == -1 || lastCardIndex == -1) { - _focusLastCard(); - notifyListeners(); - return true; - } - - isExpand = firstCardIndex > lastCardIndex; - } - } - - if (isExpand) { - final groupController = - boardController.controller(lastFocusedCard.groupId); - - if (groupController == null) { - _focusLastCard(); - notifyListeners(); - return true; - } - - final iterable = groupController.items.reversed - .skipWhile((item) => item.id != lastFocusedCard.rowId); - - if (iterable.length == 1) { - // focus on the last card in the previous group - final group = boardController.groupDatas.reversed - .skipWhile((item) => item.id != lastFocusedCard.groupId) - .skip(1) - .firstWhereOrNull((groupData) => groupData.items.isNotEmpty); - if (group != null) { - _focusedCards.add( - GroupedRowId( - rowId: group.items.last.id, - groupId: group.id, - ), - ); - } - } else { - _focusedCards.add( - GroupedRowId( - rowId: iterable.elementAt(1).id, - groupId: lastFocusedCard.groupId, - ), - ); - } - } else { - _focusedCards.removeLast(); - } - - notifyListeners(); - - return true; - } - - bool clear() { - _deepCopy(); - _focusedCards.clear(); - notifyListeners(); - return true; - } - - void _focusFirstCard() { - _focusedCards.clear(); - final firstGroup = boardController.groupDatas - .firstWhereOrNull((group) => group.items.isNotEmpty); - final firstCard = firstGroup?.items.firstOrNull; - if (firstCard != null) { - _focusedCards - .add(GroupedRowId(rowId: firstCard.id, groupId: firstGroup!.id)); - } - } - - void _focusLastCard() { - _focusedCards.clear(); - final lastGroup = boardController.groupDatas - .lastWhereOrNull((group) => group.items.isNotEmpty); - final lastCard = lastGroup?.items.lastOrNull; - if (lastCard != null) { - _focusedCards - .add(GroupedRowId(rowId: lastCard.id, groupId: lastGroup!.id)); - } - } - - void _deepCopy() { - _focusedCards = [..._focusedCards]; - } -} 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 deleted file mode 100644 index 1a0d1a3163..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart +++ /dev/null @@ -1,510 +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/database/application/cell/cell_controller.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/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: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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -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) { - final databaseController = context.read().databaseController; - return BlocSelector( - selector: (state) => state.maybeMap( - orElse: () => null, - ready: (value) => value.layoutSettings, - ), - builder: (context, layoutSettings) { - if (layoutSettings == null) { - return const SizedBox.shrink(); - } - final isCollapsed = layoutSettings.collapseHiddenGroups; - final leftPadding = margin.left + - context.read().horizontalPadding; - return AnimatedSize( - alignment: AlignmentDirectional.topStart, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 150), - child: isCollapsed - ? SizedBox( - height: 50, - child: Padding( - padding: const EdgeInsets.only(left: 80, right: 8), - child: Center( - child: _collapseExpandIcon(context, isCollapsed), - ), - ), - ) - : Container( - 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, - ), - ), - _collapseExpandIcon(context, isCollapsed), - ], - ), - ), - _hiddenGroupList(databaseController), - ], - ), - ), - ); - }, - ); - } - - 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 - ? LocaleKeys.board_hiddenGroupSection_expandTooltip.tr() - : LocaleKeys.board_hiddenGroupSection_collapseTooltip.tr(), - preferBelow: false, - child: FlowyIconButton( - width: 20, - height: 20, - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - onPressed: () => context - .read() - .add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)), - icon: FlowySvg( - isCollapsed - ? FlowySvgs.hamburger_s_s - : FlowySvgs.pull_left_outlined_s, - ), - ), - ); - } -} - -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) { - return BlocBuilder( - builder: (context, state) { - return state.maybeMap( - orElse: () => const SizedBox.shrink(), - ready: (state) => ReorderableListView.builder( - proxyDecorator: (child, index, animation) => Material( - color: Colors.transparent, - child: Stack( - children: [ - child, - MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: const SizedBox.expand(), - ), - ], - ), - ), - shrinkWrap: shrinkWrap, - buildDefaultDragHandles: false, - itemCount: state.hiddenGroups.length, - itemBuilder: (_, index) => Padding( - padding: const EdgeInsets.only(bottom: 4), - key: ValueKey("hiddenGroup${state.hiddenGroups[index].groupId}"), - child: HiddenGroupCard( - group: state.hiddenGroups[index], - index: index, - bloc: context.read(), - ), - ), - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex--; - } - final fromGroupId = state.hiddenGroups[oldIndex].groupId; - final toGroupId = state.hiddenGroups[newIndex].groupId; - context - .read() - .add(BoardEvent.reorderGroup(fromGroupId, toGroupId)); - }, - ), - ); - }, - ); - } -} - -class HiddenGroupCard extends StatefulWidget { - const HiddenGroupCard({ - super.key, - required this.group, - required this.index, - required this.bloc, - }); - - final GroupPB group; - final BoardBloc bloc; - final int index; - - @override - State createState() => _HiddenGroupCardState(); -} - -class _HiddenGroupCardState extends State { - final PopoverController _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - 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, - ), - ); - } -} - -class HiddenGroupButtonContent extends StatelessWidget { - const HiddenGroupButtonContent({ - super.key, - required this.popoverController, - required this.groupId, - required this.index, - required this.bloc, - }); - - final PopoverController popoverController; - final String groupId; - final int index; - final BoardBloc bloc; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: popoverController.show, - child: FlowyHover( - builder: (context, isHovering) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - builder: (context, state) { - return state.maybeMap( - orElse: () => const SizedBox.shrink(), - ready: (state) { - final group = state.hiddenGroups.firstWhereOrNull( - (g) => g.groupId == groupId, - ); - if (group == null) { - return const SizedBox.shrink(); - } - - return SizedBox( - height: 32, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 3, - ), - child: Row( - children: [ - HiddenGroupCardActions( - isVisible: isHovering, - index: index, - ), - const HSpace(4), - 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, - ), - ], - ), - ), - if (isHovering) ...[ - const HSpace(6), - FlowyIconButton( - width: 20, - icon: const FlowySvg( - FlowySvgs.show_m, - size: Size.square(16), - ), - onPressed: () => - context.read().add( - BoardEvent.setGroupVisibility( - group, - true, - ), - ), - ), - ], - ], - ), - ), - ); - }, - ); - }, - ), - ); - }, - ), - ), - ); - } -} - -class HiddenGroupCardActions extends StatelessWidget { - const HiddenGroupCardActions({ - super.key, - required this.isVisible, - required this.index, - }); - - final bool isVisible; - final int index; - - @override - Widget build(BuildContext context) { - return ReorderableDragStartListener( - index: index, - enabled: isVisible, - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: SizedBox( - height: 14, - width: 14, - child: isVisible - ? FlowySvg( - FlowySvgs.drag_element_s, - color: Theme.of(context).hintColor, - ) - : const SizedBox.shrink(), - ), - ), - ); - } -} - -class HiddenGroupPopupItemList extends StatelessWidget { - const HiddenGroupPopupItemList({ - super.key, - required this.groupId, - required this.viewId, - required this.primaryFieldId, - required this.rowCache, - }); - - final String groupId; - final String viewId; - final String primaryFieldId; - final RowCache rowCache; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.maybeMap( - orElse: () => const SizedBox.shrink(), - ready: (state) { - final group = state.hiddenGroups.firstWhereOrNull( - (g) => g.groupId == groupId, - ); - 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), - fontSize: 10, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - ...group.rows.map( - (item) { - final rowController = RowController( - rowMeta: item, - viewId: viewId, - rowCache: rowCache, - ); - rowController.initialize(); - - final databaseController = - context.read().databaseController; - - return HiddenGroupPopupItem( - cellContext: rowCache.loadCells(item).firstWhere( - (cellContext) => - cellContext.fieldId == primaryFieldId, - ), - rowController: rowController, - rowMeta: item, - cellBuilder: CardCellBuilder( - databaseController: databaseController, - ), - onPressed: () { - FlowyOverlay.show( - context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: RowDetailPage( - databaseController: databaseController, - rowController: rowController, - userProfile: context.read().userProfile, - ), - ), - ); - PopoverContainer.of(context).close(); - }, - ); - }, - ), - ]; - - return ListView.separated( - itemBuilder: (context, index) => cells[index], - itemCount: cells.length, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - shrinkWrap: true, - ); - }, - ); - }, - ); - } -} - -class HiddenGroupPopupItem extends StatelessWidget { - const HiddenGroupPopupItem({ - super.key, - required this.rowMeta, - required this.cellContext, - required this.onPressed, - required this.cellBuilder, - required this.rowController, - }); - - final RowMetaPB rowMeta; - final CellContext cellContext; - final RowController rowController; - final CardCellBuilder cellBuilder; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 26, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - text: cellBuilder.build( - cellContext: cellContext, - styleMap: {FieldType.RichText: _titleCellStyle(context)}, - hasNotes: false, - ), - onTap: onPressed, - ), - ); - } - - TextCardCellStyle _titleCellStyle(BuildContext context) { - return TextCardCellStyle( - padding: EdgeInsets.zero, - textStyle: Theme.of(context).textTheme.bodyMedium!, - titleTextStyle: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 11, overflow: TextOverflow.ellipsis), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_shortcut_container.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_shortcut_container.dart deleted file mode 100644 index c69269a1b7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_shortcut_container.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy/plugins/shared/callback_shortcuts.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'board_focus_scope.dart'; - -class BoardShortcutContainer extends StatelessWidget { - const BoardShortcutContainer({ - super.key, - required this.focusScope, - required this.child, - }); - - final BoardFocusScope focusScope; - final Widget child; - - @override - Widget build(BuildContext context) { - return AFCallbackShortcuts( - bindings: _shortcutBindings(context), - child: FocusScope( - child: Focus( - child: Builder( - builder: (context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - final focusNode = Focus.of(context); - focusNode.requestFocus(); - focusScope.clear(); - }, - child: child, - ); - }, - ), - ), - ), - ); - } - - Map _shortcutBindings( - BuildContext context, - ) { - return { - const SingleActivator(LogicalKeyboardKey.arrowUp): - focusScope.focusPrevious, - const SingleActivator(LogicalKeyboardKey.arrowDown): focusScope.focusNext, - const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): - focusScope.adjustRangeUp, - const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): - focusScope.adjustRangeDown, - const SingleActivator(LogicalKeyboardKey.escape): focusScope.clear, - const SingleActivator(LogicalKeyboardKey.delete): () => - _removeHandler(context), - const SingleActivator(LogicalKeyboardKey.backspace): () => - _removeHandler(context), - SingleActivator( - LogicalKeyboardKey.arrowUp, - shift: true, - meta: Platform.isMacOS, - control: !Platform.isMacOS, - ): () => _shiftCmdUpHandler(context), - const SingleActivator(LogicalKeyboardKey.enter): () => - _enterHandler(context), - const SingleActivator(LogicalKeyboardKey.numpadEnter): () => - _enterHandler(context), - const SingleActivator(LogicalKeyboardKey.enter, shift: true): () => - _shiftEnterHandler(context), - const SingleActivator(LogicalKeyboardKey.comma): () => - _moveGroupToAdjacentGroup(context, true), - const SingleActivator(LogicalKeyboardKey.period): () => - _moveGroupToAdjacentGroup(context, false), - const SingleActivator(LogicalKeyboardKey.keyE): () => - _keyEHandler(context), - const SingleActivator(LogicalKeyboardKey.keyN): () => - _keyNHandler(context), - }; - } - - bool _keyEHandler(BuildContext context) { - if (focusScope.value.length != 1) { - return false; - } - context.read().startEditingRow(focusScope.value.first); - return true; - } - - bool _keyNHandler(BuildContext context) { - if (focusScope.value.length != 1) { - return false; - } - context - .read() - .startCreateBottomRow(focusScope.value.first.groupId); - focusScope.clear(); - return true; - } - - bool _enterHandler(BuildContext context) { - if (focusScope.value.length != 1) { - return false; - } - context - .read() - .openCardWithRowId(focusScope.value.first.rowId); - return true; - } - - bool _shiftEnterHandler(BuildContext context) { - if (focusScope.value.isEmpty) { - context - .read() - .createRow(null, CreateBoardCardRelativePosition.after); - } else if (focusScope.value.length == 1) { - context.read().createRow( - focusScope.value.first, - CreateBoardCardRelativePosition.after, - ); - } else { - return false; - } - return true; - } - - bool _shiftCmdUpHandler(BuildContext context) { - if (focusScope.value.isEmpty) { - context - .read() - .createRow(null, CreateBoardCardRelativePosition.before); - } else if (focusScope.value.length == 1) { - context.read().createRow( - focusScope.value.first, - CreateBoardCardRelativePosition.before, - ); - } else { - return false; - } - return true; - } - - bool _removeHandler(BuildContext context) { - if (focusScope.value.length != 1) { - return false; - } - - NavigatorOkCancelDialog( - message: LocaleKeys.grid_row_deleteCardPrompt.tr(), - onOkPressed: () { - context.read().add(BoardEvent.deleteCards(focusScope.value)); - }, - ).show(context); - - return true; - } - - bool _moveGroupToAdjacentGroup(BuildContext context, bool toPrevious) { - if (focusScope.value.length != 1) { - return false; - } - context.read().add( - BoardEvent.moveGroupToAdjacentGroup( - focusScope.value.first, - toPrevious, - ), - ); - focusScope.clear(); - return true; - } -} 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 deleted file mode 100644 index 45157b1a47..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart +++ /dev/null @@ -1,531 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/cell_cache.dart'; -import 'package:appflowy/plugins/database/application/defines.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_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-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'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../application/database_controller.dart'; -import '../../application/row/row_cache.dart'; - -part 'calendar_bloc.freezed.dart'; - -class CalendarBloc extends Bloc { - CalendarBloc({required this.databaseController}) - : super(CalendarState.initial()) { - _dispatch(); - } - - final DatabaseController databaseController; - Map fieldInfoByFieldId = {}; - - // Getters - String get viewId => databaseController.viewId; - FieldController get fieldController => databaseController.fieldController; - 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(); - }, - didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) { - // If the field id changed, reload all events - if (state.settings?.fieldId != settings.fieldId) { - _loadAllEvents(); - } - emit(state.copyWith(settings: settings)); - }, - didReceiveDatabaseUpdate: (DatabasePB database) { - emit(state.copyWith(database: database)); - }, - didLoadAllEvents: (events) { - final calenderEvents = _calendarEventDataFromEventPBs(events); - emit( - state.copyWith( - initialEvents: calenderEvents, - allEvents: calenderEvents, - ), - ); - }, - createEvent: (DateTime date) async { - await _createEvent(date); - }, - duplicateEvent: (String viewId, String rowId) async { - final result = await RowBackendService.duplicateRow(viewId, rowId); - result.fold( - (_) => null, - (e) => Log.error('Failed to duplicate event: $e', e), - ); - }, - deleteEvent: (String viewId, String rowId) async { - final result = await RowBackendService.deleteRows(viewId, [rowId]); - result.fold( - (_) => null, - (e) => Log.error('Failed to delete event: $e', e), - ); - }, - newEventPopupDisplayed: () { - emit(state.copyWith(editingEvent: null)); - }, - moveEvent: (CalendarDayEvent event, DateTime date) async { - await _moveEvent(event, date); - }, - didCreateEvent: (CalendarEventData event) { - emit(state.copyWith(editingEvent: event)); - }, - updateCalendarLayoutSetting: - (CalendarLayoutSettingPB layoutSetting) async { - await _updateCalendarLayoutSetting(layoutSetting); - }, - didUpdateEvent: (CalendarEventData eventData) { - final allEvents = [...state.allEvents]; - final index = allEvents.indexWhere( - (element) => element.event!.eventId == eventData.event!.eventId, - ); - if (index != -1) { - allEvents[index] = eventData; - } - emit(state.copyWith(allEvents: allEvents, updateEvent: eventData)); - }, - didDeleteEvents: (List deletedRowIds) { - final events = [...state.allEvents]; - events.retainWhere( - (element) => !deletedRowIds.contains(element.event!.eventId), - ); - emit( - state.copyWith( - allEvents: events, - deleteEventIds: deletedRowIds, - ), - ); - emit(state.copyWith(deleteEventIds: const [])); - }, - didReceiveEvent: (CalendarEventData event) { - emit( - state.copyWith( - allEvents: [...state.allEvents, event], - newEvent: event, - ), - ); - emit(state.copyWith(newEvent: null)); - }, - openRowDetail: (row) { - emit(state.copyWith(openRow: row)); - emit(state.copyWith(openRow: null)); - }, - ); - }, - ); - } - - FieldInfo? _getCalendarFieldInfo(String fieldId) { - final fieldInfos = databaseController.fieldController.fieldInfos; - final index = fieldInfos.indexWhere( - (element) => element.field.id == fieldId, - ); - if (index != -1) { - return fieldInfos[index]; - } else { - return null; - } - } - - Future _openDatabase(Emitter emit) async { - final result = await databaseController.open(); - result.fold( - (database) { - databaseController.setIsLoading(false); - emit( - state.copyWith( - loadingState: LoadingState.finish(FlowyResult.success(null)), - ), - ); - }, - (err) => emit( - state.copyWith( - loadingState: LoadingState.finish(FlowyResult.failure(err)), - ), - ), - ); - } - - Future _createEvent(DateTime date) async { - final settings = state.settings; - if (settings == null) { - Log.warn('Calendar settings not found'); - return; - } - final dateField = _getCalendarFieldInfo(settings.fieldId); - if (dateField != null) { - final newRow = await RowBackendService.createRow( - viewId: viewId, - withCells: (builder) => builder.insertDate(dateField, date), - ).then( - (result) => result.fold( - (newRow) => newRow, - (err) { - Log.error(err); - return null; - }, - ), - ); - - if (newRow != null) { - final event = await _loadEvent(newRow.id); - if (event != null && !isClosed) { - add(CalendarEvent.didCreateEvent(event)); - } - } - } - } - - Future _moveEvent(CalendarDayEvent event, DateTime date) async { - final timestamp = _eventTimestamp(event, date); - final payload = MoveCalendarEventPB( - cellPath: CellIdPB( - viewId: viewId, - rowId: event.eventId, - fieldId: event.dateFieldId, - ), - timestamp: timestamp, - ); - return DatabaseEventMoveCalendarEvent(payload).send().then((result) { - return result.fold( - (_) async { - final modifiedEvent = await _loadEvent(event.eventId); - add(CalendarEvent.didUpdateEvent(modifiedEvent!)); - }, - (err) { - Log.error(err); - return null; - }, - ); - }); - } - - Future _updateCalendarLayoutSetting( - CalendarLayoutSettingPB layoutSetting, - ) async { - return databaseController.updateLayoutSetting( - calendarLayoutSetting: layoutSetting, - ); - } - - 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; - }, - ); - } - - void _loadAllEvents() async { - final payload = CalendarEventRequestPB.create()..viewId = viewId; - final result = await DatabaseEventGetAllCalendarEvents(payload).send(); - result.fold( - (events) { - if (!isClosed) { - add(CalendarEvent.didLoadAllEvents(events.items)); - } - }, - (r) => Log.error(r), - ); - } - - List> _calendarEventDataFromEventPBs( - List eventPBs, - ) { - final calendarEvents = >[]; - for (final eventPB in eventPBs) { - final event = _calendarEventDataFromEventPB(eventPB); - if (event != null) { - calendarEvents.add(event); - } - } - return calendarEvents; - } - - CalendarEventData? _calendarEventDataFromEventPB( - CalendarEventPB eventPB, - ) { - final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId]; - if (fieldInfo == null) { - return null; - } - - // timestamp is stored as seconds, but constructor requires milliseconds - final date = DateTime.fromMillisecondsSinceEpoch( - eventPB.timestamp.toInt() * 1000, - ); - - final eventData = CalendarDayEvent( - event: eventPB, - eventId: eventPB.rowMeta.id, - dateFieldId: eventPB.dateFieldId, - date: date, - ); - - return CalendarEventData( - title: eventPB.title, - date: date, - event: eventData, - ); - } - - void _startListening() { - _databaseCallbacks = DatabaseCallbacks( - onDatabaseChanged: (database) { - if (isClosed) return; - }, - onFieldsChanged: (fieldInfos) { - if (isClosed) { - return; - } - fieldInfoByFieldId = { - for (final fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo, - }; - }, - onRowsCreated: (rows) 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)); - } - } - } - }, - onRowsDeleted: (rowIds) { - if (isClosed) { - return; - } - add(CalendarEvent.didDeleteEvents(rowIds)); - }, - onRowsUpdated: (rowIds, reason) async { - if (isClosed) { - return; - } - for (final id in rowIds) { - final event = await _loadEvent(id); - if (event != null) { - if (isEventDayChanged(event)) { - add(CalendarEvent.didDeleteEvents([id])); - add(CalendarEvent.didReceiveEvent(event)); - } else { - add(CalendarEvent.didUpdateEvent(event)); - } - } - } - }, - 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( - onLayoutSettingsChanged: _didReceiveLayoutSetting, - ); - - databaseController.addListener( - onDatabaseChanged: _databaseCallbacks, - onLayoutSettingsChanged: _layoutSettingCallbacks, - ); - } - - void _didReceiveLayoutSetting(DatabaseLayoutSettingPB layoutSetting) { - if (layoutSetting.hasCalendar()) { - if (isClosed) { - return; - } - add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar)); - } - } - - bool isEventDayChanged(CalendarEventData event) { - final index = state.allEvents.indexWhere( - (element) => element.event!.eventId == event.event!.eventId, - ); - if (index == -1) { - return false; - } - 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; - return Int64(date.millisecondsSinceEpoch ~/ 1000 + time); - } -} - -typedef Events = List>; - -@freezed -class CalendarEvent with _$CalendarEvent { - const factory CalendarEvent.initial() = _InitialCalendar; - - // Called after loading the calendar layout setting from the backend - const factory CalendarEvent.didReceiveCalendarSettings( - CalendarLayoutSettingPB settings, - ) = _ReceiveCalendarSettings; - - // Called after loading all the current evnets - const factory CalendarEvent.didLoadAllEvents(List events) = - _ReceiveCalendarEvents; - - // Called when specific event was updated - const factory CalendarEvent.didUpdateEvent( - CalendarEventData event, - ) = _DidUpdateEvent; - - // Called after creating a new event - const factory CalendarEvent.didCreateEvent( - CalendarEventData event, - ) = _DidReceiveNewEvent; - - // Called after creating a new event - const factory CalendarEvent.newEventPopupDisplayed() = - _NewEventPopupDisplayed; - - // Called when receive a new event - const factory CalendarEvent.didReceiveEvent( - CalendarEventData event, - ) = _DidReceiveEvent; - - // Called when deleting events - const factory CalendarEvent.didDeleteEvents(List rowIds) = - _DidDeleteEvents; - - // Called when creating a new event - const factory CalendarEvent.createEvent(DateTime date) = _CreateEvent; - - // Called when moving an event - const factory CalendarEvent.moveEvent(CalendarDayEvent event, DateTime date) = - _MoveEvent; - - // Called when updating the calendar's layout settings - const factory CalendarEvent.updateCalendarLayoutSetting( - CalendarLayoutSettingPB layoutSetting, - ) = _UpdateCalendarLayoutSetting; - - const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = - _ReceiveDatabaseUpdate; - - const factory CalendarEvent.duplicateEvent(String viewId, String rowId) = - _DuplicateEvent; - - const factory CalendarEvent.deleteEvent(String viewId, String rowId) = - _DeleteEvent; - - const factory CalendarEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail; -} - -@freezed -class CalendarState with _$CalendarState { - const factory CalendarState({ - required DatabasePB? database, - // events by row id - required Events allEvents, - required Events initialEvents, - CalendarEventData? editingEvent, - CalendarEventData? newEvent, - CalendarEventData? updateEvent, - required List deleteEventIds, - required CalendarLayoutSettingPB? settings, - required RowMetaPB? openRow, - required LoadingState loadingState, - required FlowyError? noneOrError, - }) = _CalendarState; - - factory CalendarState.initial() => const CalendarState( - database: null, - allEvents: [], - initialEvents: [], - deleteEventIds: [], - settings: null, - openRow: null, - noneOrError: null, - loadingState: LoadingState.loading(), - ); -} - -@freezed -class CalendarDayEvent with _$CalendarDayEvent { - const factory CalendarDayEvent({ - required CalendarEventPB event, - required String dateFieldId, - required String eventId, - required DateTime date, - }) = _CalendarDayEvent; -} 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 deleted file mode 100644 index 48e159475c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart +++ /dev/null @@ -1,115 +0,0 @@ -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/widgets/setting/field_visibility_extension.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'calendar_event_editor_bloc.freezed.dart'; - -class CalendarEventEditorBloc - extends Bloc { - CalendarEventEditorBloc({ - required this.fieldController, - required this.rowController, - required this.layoutSettings, - }) : super(CalendarEventEditorState.initial()) { - _dispatch(); - } - - final FieldController fieldController; - final RowController rowController; - final CalendarLayoutSettingPB layoutSettings; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () { - rowController.initialize(); - - _startListening(); - final primaryFieldId = fieldController.fieldInfos - .firstWhere((fieldInfo) => fieldInfo.isPrimary) - .id; - final cells = rowController - .loadCells() - .where( - (cellContext) => - _filterCellContext(cellContext, primaryFieldId), - ) - .toList(); - add(CalendarEventEditorEvent.didReceiveCellDatas(cells)); - }, - didReceiveCellDatas: (cells) { - emit(state.copyWith(cells: cells)); - }, - delete: () async { - final result = await RowBackendService.deleteRows( - rowController.viewId, - [rowController.rowId], - ); - result.fold((l) => null, (err) => Log.error(err)); - }, - ); - }, - ); - } - - void _startListening() { - rowController.addListener( - onRowChanged: (cells, reason) { - if (isClosed) { - return; - } - final primaryFieldId = fieldController.fieldInfos - .firstWhere((fieldInfo) => fieldInfo.isPrimary) - .id; - final cellData = cells - .where( - (cellContext) => _filterCellContext(cellContext, primaryFieldId), - ) - .toList(); - add(CalendarEventEditorEvent.didReceiveCellDatas(cellData)); - }, - ); - } - - bool _filterCellContext(CellContext cellContext, String primaryFieldId) { - return fieldController - .getField(cellContext.fieldId)! - .fieldSettings! - .visibility - .isVisibleState() || - cellContext.fieldId == layoutSettings.fieldId || - cellContext.fieldId == primaryFieldId; - } - - @override - Future close() async { - await rowController.dispose(); - return super.close(); - } -} - -@freezed -class CalendarEventEditorEvent with _$CalendarEventEditorEvent { - const factory CalendarEventEditorEvent.initial() = _Initial; - const factory CalendarEventEditorEvent.didReceiveCellDatas( - List cells, - ) = _DidReceiveCellDatas; - const factory CalendarEventEditorEvent.delete() = _Delete; -} - -@freezed -class CalendarEventEditorState with _$CalendarEventEditorState { - const factory CalendarEventEditorState({ - required List cells, - }) = _CalendarEventEditorState; - - factory CalendarEventEditorState.initial() => - const CalendarEventEditorState(cells: []); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart deleted file mode 100644 index c1288b157d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/domain/layout_setting_listener.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; - -part 'calendar_setting_bloc.freezed.dart'; - -class CalendarSettingBloc - extends Bloc { - CalendarSettingBloc({required DatabaseController databaseController}) - : _databaseController = databaseController, - _listener = DatabaseLayoutSettingListener(databaseController.viewId), - super( - CalendarSettingState.initial( - databaseController.databaseLayoutSetting?.calendar, - ), - ) { - _dispatch(); - } - - final DatabaseController _databaseController; - final DatabaseLayoutSettingListener _listener; - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } - - void _dispatch() { - on((event, emit) { - event.when( - initial: () { - _startListening(); - }, - didUpdateLayoutSetting: (CalendarLayoutSettingPB setting) { - emit(state.copyWith(layoutSetting: layoutSetting)); - }, - updateLayoutSetting: ( - bool? showWeekends, - bool? showWeekNumbers, - int? firstDayOfWeek, - String? layoutFieldId, - ) { - _updateLayoutSettings( - showWeekends, - showWeekNumbers, - firstDayOfWeek, - layoutFieldId, - emit, - ); - }, - ); - }); - } - - void _updateLayoutSettings( - bool? showWeekends, - bool? showWeekNumbers, - int? firstDayOfWeek, - String? layoutFieldId, - Emitter emit, - ) { - final currentSetting = state.layoutSetting; - if (currentSetting == null) { - return; - } - currentSetting.freeze(); - final newSetting = currentSetting.rebuild((setting) { - if (showWeekends != null) { - setting.showWeekends = !showWeekends; - } - - if (showWeekNumbers != null) { - setting.showWeekNumbers = !showWeekNumbers; - } - - if (firstDayOfWeek != null) { - setting.firstDayOfWeek = firstDayOfWeek; - } - - if (layoutFieldId != null) { - setting.fieldId = layoutFieldId; - } - }); - - _databaseController.updateLayoutSetting( - calendarLayoutSetting: newSetting, - ); - emit(state.copyWith(layoutSetting: newSetting)); - } - - CalendarLayoutSettingPB? get layoutSetting => - _databaseController.databaseLayoutSetting?.calendar; - - void _startListening() { - _listener.start( - onLayoutChanged: (result) { - if (isClosed) { - return; - } - - result.fold( - (setting) => add( - CalendarSettingEvent.didUpdateLayoutSetting(setting.calendar), - ), - (r) => Log.error(r), - ); - }, - ); - } -} - -@freezed -class CalendarSettingState with _$CalendarSettingState { - const factory CalendarSettingState({ - required CalendarLayoutSettingPB? layoutSetting, - }) = _CalendarSettingState; - - factory CalendarSettingState.initial( - CalendarLayoutSettingPB? layoutSettings, - ) { - return CalendarSettingState(layoutSetting: layoutSettings); - } -} - -@freezed -class CalendarSettingEvent with _$CalendarSettingEvent { - const factory CalendarSettingEvent.initial() = _Initial; - const factory CalendarSettingEvent.didUpdateLayoutSetting( - CalendarLayoutSettingPB setting, - ) = _DidUpdateLayoutSetting; - const factory CalendarSettingEvent.updateLayoutSetting({ - bool? showWeekends, - bool? showWeekNumbers, - int? firstDayOfWeek, - String? layoutFieldId, - }) = _UpdateLayoutSetting; -} 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 deleted file mode 100644 index 1e67ad9e9d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/unschedule_event_bloc.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/cell_cache.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_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:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../application/database_controller.dart'; -import '../../application/row/row_cache.dart'; - -part 'unschedule_event_bloc.freezed.dart'; - -class UnscheduleEventsBloc - extends Bloc { - UnscheduleEventsBloc({required this.databaseController}) - : super(UnscheduleEventsState.initial()) { - _dispatch(); - } - - final DatabaseController databaseController; - Map fieldInfoByFieldId = {}; - - // Getters - String get viewId => databaseController.viewId; - FieldController get fieldController => databaseController.fieldController; - 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 { - await event.when( - initial: () async { - _startListening(); - _loadAllEvents(); - }, - didLoadAllEvents: (events) { - emit( - state.copyWith( - allEvents: events, - unscheduleEvents: - events.where((element) => !element.hasTimestamp()).toList(), - ), - ); - }, - didDeleteEvents: (List deletedRowIds) { - final events = [...state.allEvents]; - events.retainWhere( - (element) => !deletedRowIds.contains(element.rowMeta.id), - ); - emit( - state.copyWith( - allEvents: events, - unscheduleEvents: - events.where((element) => !element.hasTimestamp()).toList(), - ), - ); - }, - didReceiveEvent: (CalendarEventPB event) { - final events = [...state.allEvents, event]; - emit( - state.copyWith( - allEvents: events, - unscheduleEvents: - events.where((element) => !element.hasTimestamp()).toList(), - ), - ); - }, - ); - }, - ); - } - - Future _loadEvent( - RowId rowId, - ) async { - final payload = DatabaseViewRowIdPB(viewId: viewId, rowId: rowId); - return DatabaseEventGetCalendarEvent(payload).send().then( - (result) => result.fold( - (eventPB) => eventPB, - (r) { - Log.error(r); - return null; - }, - ), - ); - } - - void _loadAllEvents() async { - final payload = CalendarEventRequestPB.create()..viewId = viewId; - final result = await DatabaseEventGetAllCalendarEvents(payload).send(); - result.fold( - (events) { - if (!isClosed) { - add(UnscheduleEventsEvent.didLoadAllEvents(events.items)); - } - }, - (r) => Log.error(r), - ); - } - - void _startListening() { - _databaseCallbacks = DatabaseCallbacks( - onRowsCreated: (rows) async { - if (isClosed) { - return; - } - for (final row in rows) { - final event = await _loadEvent(row.rowMeta.id); - if (event != null && !isClosed) { - add(UnscheduleEventsEvent.didReceiveEvent(event)); - } - } - }, - onRowsDeleted: (rowIds) { - if (isClosed) { - return; - } - add(UnscheduleEventsEvent.didDeleteEvents(rowIds)); - }, - onRowsUpdated: (rowIds, reason) async { - if (isClosed) { - return; - } - for (final id in rowIds) { - final event = await _loadEvent(id); - if (event != null) { - add(UnscheduleEventsEvent.didDeleteEvents([id])); - add(UnscheduleEventsEvent.didReceiveEvent(event)); - } - } - }, - ); - - databaseController.addListener(onDatabaseChanged: _databaseCallbacks); - } -} - -@freezed -class UnscheduleEventsEvent with _$UnscheduleEventsEvent { - const factory UnscheduleEventsEvent.initial() = _InitialCalendar; - - // Called after loading all the current evnets - const factory UnscheduleEventsEvent.didLoadAllEvents( - List events, - ) = _ReceiveUnscheduleEventsEvents; - - const factory UnscheduleEventsEvent.didDeleteEvents(List rowIds) = - _DidDeleteEvents; - - const factory UnscheduleEventsEvent.didReceiveEvent( - CalendarEventPB event, - ) = _DidReceiveEvent; -} - -@freezed -class UnscheduleEventsState with _$UnscheduleEventsState { - const factory UnscheduleEventsState({ - required DatabasePB? database, - required List allEvents, - required List unscheduleEvents, - }) = _UnscheduleEventsState; - - factory UnscheduleEventsState.initial() => const UnscheduleEventsState( - database: null, - allEvents: [], - unscheduleEvents: [], - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/calendar.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/calendar.dart deleted file mode 100644 index 54667d5f69..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/calendar.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class CalendarPluginBuilder extends PluginBuilder { - @override - Plugin build(dynamic data) { - if (data is ViewPB) { - return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); - } else { - throw FlowyPluginException.invalidData; - } - } - - @override - String get menuName => LocaleKeys.calendar_menuName.tr(); - - @override - FlowySvgData get icon => FlowySvgs.icon_calendar_s; - - @override - PluginType get pluginType => PluginType.calendar; - - @override - ViewLayoutPB get layoutType => ViewLayoutPB.Calendar; -} - -class CalendarPluginConfig implements PluginConfig { - @override - bool get creatable => true; -} 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 deleted file mode 100644 index 1d2838210d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart +++ /dev/null @@ -1,399 +0,0 @@ -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: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/time/duration.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 '../../grid/presentation/layout/sizes.dart'; -import '../application/calendar_bloc.dart'; -import 'calendar_event_card.dart'; - -class CalendarDayCard extends StatelessWidget { - const CalendarDayCard({ - super.key, - required this.viewId, - required this.isToday, - required this.isInMonth, - required this.date, - required this.rowCache, - required this.events, - required this.onCreateEvent, - required this.position, - }); - - final String viewId; - final bool isToday; - final bool isInMonth; - final DateTime date; - final RowCache rowCache; - final List events; - final void Function(DateTime) onCreateEvent; - final CellPosition position; - - @override - Widget build(BuildContext context) { - final hoverBackgroundColor = - Theme.of(context).brightness == Brightness.light - ? Theme.of(context).colorScheme.secondaryContainer - : Colors.transparent; - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return ChangeNotifierProvider( - create: (_) => _CardEnterNotifier(), - builder: (context, child) { - final child = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - _Header( - date: date, - isInMonth: isInMonth, - isToday: isToday, - ), - - // Add a separator between the header and the content. - const VSpace(6.0), - - // List of cards or empty space - if (events.isNotEmpty && !UniversalPlatform.isMobile) ...[ - _EventList( - events: events, - viewId: viewId, - rowCache: rowCache, - constraints: constraints, - ), - ] else if (events.isNotEmpty && UniversalPlatform.isMobile) ...[ - const _EventIndicator(), - ], - ], - ); - - return Stack( - children: [ - GestureDetector( - onDoubleTap: () => onCreateEvent(date), - onTap: UniversalPlatform.isMobile - ? () => _mobileOnTap(context) - : null, - child: Container( - decoration: BoxDecoration( - color: date.isWeekend - ? AFThemeExtension.of(context).calendarWeekendBGColor - : Colors.transparent, - border: _borderFromPosition(context, position), - ), - ), - ), - DragTarget( - builder: (context, candidate, __) { - return Stack( - children: [ - Container( - width: double.infinity, - height: double.infinity, - color: - candidate.isEmpty ? null : hoverBackgroundColor, - padding: const EdgeInsets.only(top: 5.0), - child: child, - ), - if (candidate.isEmpty && !UniversalPlatform.isMobile) - NewEventButton( - onCreate: () => onCreateEvent(date), - ), - ], - ); - }, - onAcceptWithDetails: (details) { - final event = details.data; - if (event.date != date) { - context - .read() - .add(CalendarEvent.moveEvent(event, date)); - } - }, - ), - MouseRegion( - onEnter: (p) => notifyEnter(context, true), - onExit: (p) => notifyEnter(context, false), - opaque: false, - hitTestBehavior: HitTestBehavior.translucent, - ), - ], - ); - }, - ); - }, - ); - } - - void _mobileOnTap(BuildContext context) { - context.push( - MobileCalendarEventsScreen.routeName, - extra: { - MobileCalendarEventsScreen.calendarBlocKey: - context.read(), - MobileCalendarEventsScreen.calendarDateKey: date, - MobileCalendarEventsScreen.calendarEventsKey: events, - MobileCalendarEventsScreen.calendarRowCacheKey: rowCache, - MobileCalendarEventsScreen.calendarViewIdKey: viewId, - }, - ); - } - - bool notifyEnter(BuildContext context, bool isEnter) => - Provider.of<_CardEnterNotifier>(context, listen: false).onEnter = isEnter; - - Border _borderFromPosition(BuildContext context, CellPosition position) { - final BorderSide borderSide = - BorderSide(color: Theme.of(context).dividerColor); - - return Border( - top: borderSide, - left: borderSide, - bottom: [ - CellPosition.bottom, - CellPosition.bottomLeft, - CellPosition.bottomRight, - ].contains(position) - ? borderSide - : BorderSide.none, - right: [ - CellPosition.topRight, - CellPosition.bottomRight, - CellPosition.right, - ].contains(position) - ? borderSide - : BorderSide.none, - ); - } -} - -class _EventIndicator extends StatelessWidget { - const _EventIndicator(); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 7, - height: 7, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).hintColor, - ), - ), - ], - ); - } -} - -class _Header extends StatelessWidget { - const _Header({ - required this.isToday, - required this.isInMonth, - required this.date, - }); - - final bool isToday; - final bool isInMonth; - final DateTime date; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: _DayBadge(isToday: isToday, isInMonth: isInMonth, date: date), - ); - } -} - -@visibleForTesting -class NewEventButton extends StatelessWidget { - const NewEventButton({super.key, required this.onCreate}); - - final VoidCallback onCreate; - - @override - Widget build(BuildContext context) { - return Consumer<_CardEnterNotifier>( - builder: (context, notifier, _) { - if (!notifier.onEnter) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.all(4.0), - child: FlowyIconButton( - onPressed: onCreate, - icon: const FlowySvg(FlowySvgs.add_s), - fillColor: Theme.of(context).colorScheme.surface, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - width: 22, - tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(), - radius: Corners.s6Border, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).brightness == Brightness.light - ? const Color(0xffd0d3d6) - : const Color(0xff59647a), - width: 0.5, - ), - ), - borderRadius: Corners.s6Border, - boxShadow: [ - BoxShadow( - spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - blurRadius: 2, - ), - BoxShadow( - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - blurRadius: 4, - ), - BoxShadow( - spreadRadius: 2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - blurRadius: 8, - ), - ], - ), - ), - ); - }, - ); - } -} - -class _DayBadge extends StatelessWidget { - const _DayBadge({ - required this.isToday, - required this.isInMonth, - required this.date, - }); - - final bool isToday; - final bool isInMonth; - final DateTime date; - - @override - Widget build(BuildContext context) { - Color dayTextColor = AFThemeExtension.of(context).onBackground; - Color monthTextColor = AFThemeExtension.of(context).onBackground; - final String monthString = - DateFormat("MMM ", context.locale.toLanguageTag()).format(date); - final String dayString = date.day.toString(); - - if (!isInMonth) { - dayTextColor = Theme.of(context).disabledColor; - monthTextColor = Theme.of(context).disabledColor; - } - if (isToday) { - dayTextColor = Theme.of(context).colorScheme.onPrimary; - } - - final double size = UniversalPlatform.isMobile ? 20 : 18; - - return SizedBox( - height: size, - child: Row( - mainAxisAlignment: UniversalPlatform.isMobile - ? MainAxisAlignment.center - : MainAxisAlignment.end, - children: [ - if (date.day == 1 && !UniversalPlatform.isMobile) - FlowyText.medium( - monthString, - fontSize: 11, - color: monthTextColor, - ), - Container( - decoration: BoxDecoration( - color: isToday ? Theme.of(context).colorScheme.primary : null, - borderRadius: BorderRadius.circular(10), - ), - width: isToday ? size : null, - height: isToday ? size : null, - child: Center( - child: FlowyText( - dayString, - fontSize: UniversalPlatform.isMobile ? 12 : 11, - color: dayTextColor, - ), - ), - ), - ], - ), - ); - } -} - -class _EventList extends StatelessWidget { - const _EventList({ - required this.events, - required this.viewId, - required this.rowCache, - required this.constraints, - }); - - final List events; - final String viewId; - final RowCache rowCache; - final BoxConstraints constraints; - - @override - Widget build(BuildContext context) { - final editingEvent = context.watch().state.editingEvent; - - return Flexible( - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: true), - child: ListView.separated( - itemBuilder: (BuildContext context, int index) { - final autoEdit = - editingEvent?.event?.eventId == events[index].eventId; - return EventCard( - databaseController: - context.read().databaseController, - event: events[index], - constraints: constraints, - autoEdit: autoEdit, - ); - }, - itemCount: events.length, - padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0), - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - shrinkWrap: true, - ), - ), - ); - } -} - -class _CardEnterNotifier extends ChangeNotifier { - _CardEnterNotifier(); - - bool _onEnter = false; - - set onEnter(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get onEnter => _onEnter; -} 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 deleted file mode 100644 index 5ef2e2c327..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ /dev/null @@ -1,224 +0,0 @@ -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: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_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'; - -class EventCard extends StatefulWidget { - const EventCard({ - super.key, - required this.databaseController, - required this.event, - required this.constraints, - required this.autoEdit, - this.isDraggable = true, - this.padding = EdgeInsets.zero, - }); - - final DatabaseController databaseController; - final CalendarDayEvent event; - final BoxConstraints constraints; - final bool autoEdit; - final bool isDraggable; - final EdgeInsets padding; - - @override - State createState() => _EventCardState(); -} - -class _EventCardState extends State { - final PopoverController _popoverController = PopoverController(); - - String get viewId => widget.databaseController.viewId; - RowCache get rowCache => widget.databaseController.rowCache; - - @override - void initState() { - super.initState(); - if (widget.autoEdit) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _popoverController.show(); - context - .read() - .add(const CalendarEvent.newEventPopupDisplayed()); - }); - } - } - - @override - Widget build(BuildContext context) { - final rowInfo = rowCache.getRow(widget.event.eventId); - if (rowInfo == null) { - return const SizedBox.shrink(); - } - - final cellBuilder = CardCellBuilder( - databaseController: widget.databaseController, - ); - - Widget card = RowCard( - // Add the key here to make sure the card is rebuilt when the cells - // in this row are updated. - key: ValueKey(widget.event.eventId), - fieldController: widget.databaseController.fieldController, - rowMeta: rowInfo.rowMeta, - viewId: viewId, - rowCache: rowCache, - isEditing: false, - cellBuilder: cellBuilder, - isCompact: true, - onTap: (context) { - if (UniversalPlatform.isMobile) { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: rowInfo.rowId, - MobileRowDetailPage.argDatabaseController: - widget.databaseController, - }, - ); - } else { - _popoverController.show(); - } - }, - styleConfiguration: RowCardStyleConfiguration( - cellStyleMap: desktopCalendarCardCellStyleMap(context), - showAccessory: false, - cardPadding: const EdgeInsets.all(6), - hoverStyle: HoverStyle( - hoverColor: Theme.of(context).brightness == Brightness.light - ? const Color(0x0F1F2329) - : const Color(0x0FEFF4FB), - foregroundColorOnHover: AFThemeExtension.of(context).onBackground, - ), - ), - onStartEditing: () {}, - onEndEditing: () {}, - userProfile: context.read().userProfile, - ); - - final decoration = BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).brightness == Brightness.light - ? const Color(0xffd0d3d6) - : const Color(0xff59647a), - width: 0.5, - ), - ), - borderRadius: Corners.s6Border, - boxShadow: [ - BoxShadow( - spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - blurRadius: 2, - ), - BoxShadow( - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - blurRadius: 4, - ), - BoxShadow( - spreadRadius: 2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - blurRadius: 8, - ), - ], - ); - - card = AppFlowyPopover( - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.rightWithCenterAligned, - controller: _popoverController, - constraints: const BoxConstraints(maxWidth: 360, maxHeight: 348), - asBarrier: true, - margin: EdgeInsets.zero, - offset: const Offset(10.0, 0), - popupBuilder: (_) { - final settings = context.watch().state.settings; - if (settings == null) { - return const SizedBox.shrink(); - } - return MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: context.read(), - ), - ], - child: CalendarEventEditor( - 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, - ), - ), - ), - ); - - if (widget.isDraggable) { - return Draggable( - data: widget.event, - feedback: Container( - constraints: BoxConstraints( - maxWidth: widget.constraints.maxWidth - 8.0, - ), - decoration: decoration, - child: Opacity( - opacity: 0.6, - child: card, - ), - ), - child: card, - ); - } - - return 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 deleted file mode 100644 index dcbe626dd8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ /dev/null @@ -1,307 +0,0 @@ -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/database_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/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_backend/protobuf/flowy-database2/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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class CalendarEventEditor extends StatelessWidget { - CalendarEventEditor({ - super.key, - required RowMetaPB rowMeta, - required this.layoutSettings, - required this.databaseController, - required this.onExpand, - }) : rowController = RowController( - rowMeta: rowMeta, - viewId: databaseController.viewId, - rowCache: databaseController.rowCache, - ), - cellBuilder = - EditableCellBuilder(databaseController: databaseController); - - final CalendarLayoutSettingPB layoutSettings; - final DatabaseController databaseController; - final RowController rowController; - final EditableCellBuilder cellBuilder; - final VoidCallback onExpand; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => CalendarEventEditorBloc( - fieldController: databaseController.fieldController, - rowController: rowController, - layoutSettings: layoutSettings, - )..add(const CalendarEventEditorEvent.initial()), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - EventEditorControls( - rowController: rowController, - databaseController: databaseController, - onExpand: onExpand, - ), - Flexible( - child: EventPropertyList( - fieldController: databaseController.fieldController, - dateFieldId: layoutSettings.fieldId, - cellBuilder: cellBuilder, - ), - ), - ], - ), - ); - } -} - -class EventEditorControls extends StatelessWidget { - const EventEditorControls({ - 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) { - return Padding( - padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FlowyTooltip( - message: LocaleKeys.calendar_duplicateEvent.tr(), - child: FlowyIconButton( - width: 20, - icon: FlowySvg( - FlowySvgs.m_duplicate_s, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, - ), - onPressed: () { - context.read().add( - CalendarEvent.duplicateEvent( - rowController.viewId, - rowController.rowId, - ), - ); - PopoverContainer.of(context).close(); - }, - ), - ), - 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(); - }, - ); - }, - ), - const HSpace(8.0), - FlowyIconButton( - width: 20, - icon: FlowySvg( - FlowySvgs.full_view_s, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, - ), - onPressed: () { - PopoverContainer.of(context).close(); - onExpand.call(); - }, - ), - ], - ), - ); - } -} - -class EventPropertyList extends StatelessWidget { - const EventPropertyList({ - super.key, - required this.fieldController, - required this.dateFieldId, - required this.cellBuilder, - }); - - final FieldController fieldController; - final String dateFieldId; - final EditableCellBuilder cellBuilder; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final primaryFieldId = fieldController.fieldInfos - .firstWhereOrNull((fieldInfo) => fieldInfo.isPrimary)! - .id; - final reorderedList = List.from(state.cells) - ..retainWhere((cell) => cell.fieldId != primaryFieldId); - - final primaryCellContext = state.cells - .firstWhereOrNull((cell) => cell.fieldId == primaryFieldId); - final dateFieldIndex = - reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId); - - if (primaryCellContext == null || dateFieldIndex == -1) { - return const SizedBox.shrink(); - } - - reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex)); - - final children = [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), - child: cellBuilder.buildCustom( - primaryCellContext, - skinMap: EditableCellSkinMap(textSkin: _TitleTextCellSkin()), - ), - ), - ...reorderedList.map( - (cellContext) => PropertyCell( - fieldController: fieldController, - cellContext: cellContext, - cellBuilder: cellBuilder, - ), - ), - ]; - - return ListView( - shrinkWrap: true, - padding: const EdgeInsets.only(bottom: 16.0), - children: children, - ); - }, - ); - } -} - -class PropertyCell extends StatefulWidget { - const PropertyCell({ - super.key, - required this.fieldController, - required this.cellContext, - required this.cellBuilder, - }); - - final FieldController fieldController; - final CellContext cellContext; - final EditableCellBuilder cellBuilder; - - @override - State createState() => _PropertyCellState(); -} - -class _PropertyCellState extends State { - @override - Widget build(BuildContext context) { - final fieldInfo = - widget.fieldController.getField(widget.cellContext.fieldId)!; - final cell = widget.cellBuilder - .buildStyled(widget.cellContext, EditableCellStyle.desktopRowDetail); - - final gesture = GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => cell.requestFocus.notify(), - child: AccessoryHover( - fieldType: fieldInfo.fieldType, - child: cell, - ), - ); - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - constraints: const BoxConstraints(minHeight: 28), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 88, - height: 28, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6), - child: Row( - children: [ - FieldIcon( - fieldInfo: fieldInfo, - dimension: 14, - ), - const HSpace(4.0), - Expanded( - child: FlowyText.regular( - fieldInfo.name, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - fontSize: 11, - ), - ), - ], - ), - ), - ), - const HSpace(8), - Expanded(child: gesture), - ], - ), - ); - } -} - -class _TitleTextCellSkin extends IEditableTextCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TextCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return FlowyTextField( - controller: textEditingController, - textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14), - focusNode: focusNode, - hintText: LocaleKeys.calendar_defaultNewCalendarTitle.tr(), - onEditingComplete: () { - bloc.add(TextCellEvent.updateText(textEditingController.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 deleted file mode 100644 index 1876332d01..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ /dev/null @@ -1,658 +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/database/card/card.dart'; -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_backend/protobuf/flowy-folder/view.pb.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: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'; -import 'calendar_day.dart'; -import 'layout/sizes.dart'; -import 'toolbar/calendar_setting_bar.dart'; - -class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { - final _toggleExtension = ToggleExtensionNotifier(); - - @override - Widget content( - BuildContext context, - ViewPB view, - DatabaseController controller, - bool shrinkWrap, - String? initialRowId, - ) { - return CalendarPage( - key: _makeValueKey(controller), - view: view, - databaseController: controller, - shrinkWrap: shrinkWrap, - ); - } - - @override - Widget settingBar(BuildContext context, DatabaseController controller) { - return CalendarSettingBar( - key: _makeValueKey(controller), - databaseController: controller, - toggleExtension: _toggleExtension, - ); - } - - @override - Widget settingBarExtension( - BuildContext context, - DatabaseController controller, - ) { - return DatabaseViewSettingExtension( - key: _makeValueKey(controller), - viewId: controller.viewId, - databaseController: controller, - toggleExtension: _toggleExtension, - ); - } - - @override - void dispose() { - _toggleExtension.dispose(); - super.dispose(); - } - - ValueKey _makeValueKey(DatabaseController controller) { - return ValueKey(controller.viewId); - } -} - -class CalendarPage extends StatefulWidget { - const CalendarPage({ - super.key, - required this.view, - required this.databaseController, - this.shrinkWrap = false, - }); - - final ViewPB view; - final DatabaseController databaseController; - final bool shrinkWrap; - - @override - State createState() => _CalendarPageState(); -} - -class _CalendarPageState extends State { - final _eventController = EventController(); - late final CalendarBloc _calendarBloc; - GlobalKey? _calendarState; - - @override - void initState() { - super.initState(); - _calendarState = GlobalKey(); - _calendarBloc = CalendarBloc( - databaseController: widget.databaseController, - )..add(const CalendarEvent.initial()); - } - - @override - void dispose() { - _calendarBloc.close(); - super.dispose(); - } - - @override - 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: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => p.initialEvents != c.initialEvents, - listener: (context, state) { - _eventController.removeWhere((_) => true); - _eventController.addAll(state.initialEvents); - }, - ), - BlocListener( - listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds, - listener: (context, state) { - _eventController.removeWhere( - (element) => - state.deleteEventIds.contains(element.event!.eventId), - ); - }, - ), - BlocListener( - // Event create by click the + button or double click on the - // calendar - listenWhen: (p, c) => p.newEvent != c.newEvent, - listener: (context, state) { - if (state.newEvent != null) { - _eventController.add(state.newEvent!); - } - }, - ), - BlocListener( - // When an event is rescheduled - listenWhen: (p, c) => p.updateEvent != c.updateEvent, - listener: (context, state) { - if (state.updateEvent != null) { - _eventController.removeWhere( - (element) => - element.event!.eventId == - state.updateEvent!.event!.eventId, - ); - _eventController.add(state.updateEvent!); - } - }, - ), - 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) { - return ValueListenableBuilder( - valueListenable: widget.databaseController.isLoading, - builder: (_, value, ___) { - if (value) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - return _buildCalendar( - context, - _eventController, - state.settings?.firstDayOfWeek ?? 0, - ); - }, - ); - }, - ), - ), - ), - ); - } - - Widget _buildCalendar( - BuildContext context, - EventController eventController, - int firstDayOfWeek, - ) { - 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, - child: ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: MonthView( - key: _calendarState, - controller: _eventController, - width: constraints.maxWidth, - cellAspectRatio: UniversalPlatform.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, - ), - useAvailableVerticalSpace: widget.shrinkWrap, - ), - ), - ); - }, - ); - } - - Widget _headerNavigatorBuilder(DateTime currentMonth) { - return SizedBox( - height: 24, - child: Row( - children: [ - GestureDetector( - onTap: UniversalPlatform.isMobile - ? () => showMobileBottomSheet( - context, - title: LocaleKeys.calendar_quickJumpYear.tr(), - showHeader: true, - showCloseButton: true, - builder: (_) => SizedBox( - height: 200, - child: YearPicker( - firstDate: CalendarConstants.epochDate.withoutTime, - lastDate: CalendarConstants.maxDate.withoutTime, - selectedDate: currentMonth, - currentDate: DateTime.now(), - onChanged: (newDate) { - _calendarState?.currentState?.jumpToMonth(newDate); - context.pop(); - }, - ), - ), - ) - : null, - child: Row( - children: [ - FlowyText.medium( - DateFormat('MMMM y', context.locale.toLanguageTag()) - .format(currentMonth), - ), - if (UniversalPlatform.isMobile) ...[ - const HSpace(6), - const FlowySvg(FlowySvgs.arrow_down_s), - ], - ], - ), - ), - const Spacer(), - FlowyIconButton( - width: CalendarSize.navigatorButtonWidth, - height: CalendarSize.navigatorButtonHeight, - icon: const FlowySvg(FlowySvgs.arrow_left_s), - tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onPressed: () => _calendarState?.currentState?.previousPage(), - ), - FlowyTextButton( - LocaleKeys.calendar_navigation_today.tr(), - fillColor: Colors.transparent, - fontWeight: FontWeight.w400, - fontSize: 10, - fontColor: AFThemeExtension.of(context).textColor, - tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(), - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onPressed: () => - _calendarState?.currentState?.animateToMonth(DateTime.now()), - ), - FlowyIconButton( - width: CalendarSize.navigatorButtonWidth, - height: CalendarSize.navigatorButtonHeight, - icon: const FlowySvg(FlowySvgs.arrow_right_s), - tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onPressed: () => _calendarState?.currentState?.nextPage(), - ), - const HSpace(6.0), - UnscheduledEventsButton( - databaseController: widget.databaseController, - ), - ], - ), - ); - } - - Widget _headerWeekDayBuilder(day) { - // incoming day starts from Monday, the symbols start from Sunday - final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; - String weekDayString = symbols.WEEKDAYS[(day + 1) % 7]; - - if (UniversalPlatform.isMobile) { - weekDayString = weekDayString.substring(0, 3); - } - - return Center( - child: Padding( - padding: CalendarSize.daysOfWeekInsets, - child: FlowyText.regular( - weekDayString, - fontSize: 9, - color: Theme.of(context).hintColor, - ), - ), - ); - } - - Widget _calendarDayBuilder( - BuildContext context, - DateTime date, - List> calenderEvents, - isToday, - isInMonth, - position, - ) { - // Sort the events by timestamp. Because the database view is not - // reserving the order of the events. Reserving the order of the rows/events - // 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, - ), - ); - } - - WeekDays _weekdayFromInt(int dayOfWeek) { - // dayOfWeek starts from Sunday, WeekDays starts from Monday - return WeekDays.values[(dayOfWeek - 1) % 7]; - } -} - -void showEventDetails({ - required BuildContext context, - required DatabaseController databaseController, - required RowMetaPB rowMeta, -}) { - final rowController = RowController( - rowMeta: rowMeta, - viewId: databaseController.viewId, - rowCache: databaseController.rowCache, - ); - - FlowyOverlay.show( - context: context, - builder: (BuildContext overlayContext) { - return BlocProvider.value( - value: context.read(), - child: RowDetailPage( - rowController: rowController, - databaseController: databaseController, - userProfile: context.read().userProfile, - ), - ); - }, - ); -} - -class UnscheduledEventsButton extends StatefulWidget { - const UnscheduledEventsButton({super.key, required this.databaseController}); - - final DatabaseController databaseController; - - @override - State createState() => - _UnscheduledEventsButtonState(); -} - -class _UnscheduledEventsButtonState extends State { - final PopoverController _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => - UnscheduleEventsBloc(databaseController: widget.databaseController) - ..add(const UnscheduleEventsEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - triggerActions: PopoverTriggerFlags.none, - controller: _popoverController, - offset: const Offset(0, 8), - constraints: const BoxConstraints(maxWidth: 282, maxHeight: 600), - child: OutlinedButton( - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - side: BorderSide(color: Theme.of(context).dividerColor), - borderRadius: Corners.s6Border, - ), - side: BorderSide(color: Theme.of(context).dividerColor), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - ), - onPressed: () { - if (state.unscheduleEvents.isNotEmpty) { - if (UniversalPlatform.isMobile) { - _showUnscheduledEventsMobile(state.unscheduleEvents); - } else { - _popoverController.show(); - } - } - }, - child: FlowyTooltip( - message: LocaleKeys.calendar_settings_noDateHint.plural( - state.unscheduleEvents.length, - namedArgs: {'count': '${state.unscheduleEvents.length}'}, - ), - child: FlowyText.regular( - "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})", - fontSize: 10, - ), - ), - ), - popupBuilder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: context.read(), - ), - ], - child: UnscheduleEventsList( - databaseController: widget.databaseController, - unscheduleEvents: state.unscheduleEvents, - ), - ), - ); - }, - ), - ); - } - - void _showUnscheduledEventsMobile(List events) => - showMobileBottomSheet( - context, - builder: (_) { - return Column( - children: [ - FlowyText( - LocaleKeys.calendar_settings_unscheduledEventsTitle.tr(), - ), - UnscheduleEventsList( - databaseController: widget.databaseController, - unscheduleEvents: events, - ), - ], - ); - }, - ); -} - -class UnscheduleEventsList extends StatelessWidget { - const UnscheduleEventsList({ - super.key, - required this.unscheduleEvents, - required this.databaseController, - }); - - final List unscheduleEvents; - final DatabaseController databaseController; - - @override - Widget build(BuildContext context) { - final cells = [ - if (!UniversalPlatform.isMobile) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: FlowyText( - LocaleKeys.calendar_settings_clickToAdd.tr(), - fontSize: 10, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - ...unscheduleEvents.map( - (event) => UnscheduledEventCell( - event: event, - onPressed: () { - if (UniversalPlatform.isMobile) { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: event.rowMeta.id, - MobileRowDetailPage.argDatabaseController: databaseController, - }, - ); - context.pop(); - } else { - showEventDetails( - context: context, - rowMeta: event.rowMeta, - databaseController: databaseController, - ); - PopoverContainer.of(context).close(); - } - }, - ), - ), - ]; - - final child = ListView.separated( - itemBuilder: (context, index) => cells[index], - itemCount: cells.length, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - shrinkWrap: true, - ); - - if (UniversalPlatform.isMobile) { - return Flexible(child: child); - } - - return child; - } -} - -class UnscheduledEventCell extends StatelessWidget { - const UnscheduledEventCell({ - super.key, - required this.event, - required this.onPressed, - }); - - final CalendarEventPB event; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return UniversalPlatform.isMobile - ? MobileUnscheduledEventTile(event: event, onPressed: onPressed) - : DesktopUnscheduledEventTile(event: event, onPressed: onPressed); - } -} - -class DesktopUnscheduledEventTile extends StatelessWidget { - const DesktopUnscheduledEventTile({ - super.key, - required this.event, - required this.onPressed, - }); - - final CalendarEventPB event; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 26, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - text: FlowyText( - event.title.isEmpty - ? LocaleKeys.calendar_defaultNewCalendarTitle.tr() - : event.title, - fontSize: 11, - ), - onTap: onPressed, - ), - ); - } -} - -class MobileUnscheduledEventTile extends StatelessWidget { - const MobileUnscheduledEventTile({ - super.key, - required this.event, - required this.onPressed, - }); - - final CalendarEventPB event; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return MobileSettingItem( - name: event.title.isEmpty - ? LocaleKeys.calendar_defaultNewCalendarTitle.tr() - : event.title, - onTap: onPressed, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/layout/sizes.dart deleted file mode 100644 index 73b006691f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/layout/sizes.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:flutter/widgets.dart'; - -class CalendarSize { - static double scale = 1; - - static double get headerContainerPadding => 12 * scale; - - static EdgeInsets get contentInsets => EdgeInsets.fromLTRB( - GridSize.horizontalHeaderPadding, - CalendarSize.headerContainerPadding, - GridSize.horizontalHeaderPadding, - CalendarSize.headerContainerPadding, - ); - - static EdgeInsets get contentInsetsMobile => EdgeInsets.fromLTRB( - GridSize.horizontalHeaderPadding / 2, - 0, - GridSize.horizontalHeaderPadding / 2, - 0, - ); - - static double get scrollBarSize => 8 * scale; - static double get navigatorButtonWidth => 20 * scale; - static double get navigatorButtonHeight => 24 * scale; - static EdgeInsets get daysOfWeekInsets => - EdgeInsets.only(top: 12.0 * scale, bottom: 5.0 * scale); -} 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 deleted file mode 100644 index 8a66191950..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart +++ /dev/null @@ -1,383 +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:appflowy/plugins/database/application/database_controller.dart'; -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_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_bloc/flutter_bloc.dart'; - -/// Widget that displays a list of settings that alters the appearance of the -/// calendar -class CalendarLayoutSetting extends StatefulWidget { - const CalendarLayoutSetting({ - super.key, - required this.databaseController, - }); - - final DatabaseController databaseController; - - @override - State createState() => _CalendarLayoutSettingState(); -} - -class _CalendarLayoutSettingState extends State { - final PopoverMutex popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return CalendarSettingBloc( - databaseController: widget.databaseController, - )..add(const CalendarSettingEvent.initial()); - }, - child: BlocBuilder( - builder: (context, state) { - final CalendarLayoutSettingPB? settings = state.layoutSetting; - - if (settings == null) { - return const CircularProgressIndicator(); - } - final availableSettings = _availableCalendarSettings(settings); - final bloc = context.read(); - final items = availableSettings.map((setting) { - switch (setting) { - case CalendarLayoutSettingAction.showWeekNumber: - return ShowWeekNumber( - showWeekNumbers: settings.showWeekNumbers, - onUpdated: (showWeekNumbers) => bloc.add( - CalendarSettingEvent.updateLayoutSetting( - showWeekNumbers: showWeekNumbers, - ), - ), - ); - case CalendarLayoutSettingAction.showWeekends: - return ShowWeekends( - showWeekends: settings.showWeekends, - onUpdated: (showWeekends) => bloc.add( - CalendarSettingEvent.updateLayoutSetting( - showWeekends: showWeekends, - ), - ), - ); - case CalendarLayoutSettingAction.firstDayOfWeek: - return FirstDayOfWeek( - firstDayOfWeek: settings.firstDayOfWeek, - popoverMutex: popoverMutex, - onUpdated: (firstDayOfWeek) => bloc.add( - CalendarSettingEvent.updateLayoutSetting( - firstDayOfWeek: firstDayOfWeek, - ), - ), - ); - case CalendarLayoutSettingAction.layoutField: - return LayoutDateField( - databaseController: widget.databaseController, - fieldId: settings.fieldId, - popoverMutex: popoverMutex, - onUpdated: (fieldId) => bloc.add( - CalendarSettingEvent.updateLayoutSetting( - layoutFieldId: fieldId, - ), - ), - ); - default: - return const SizedBox.shrink(); - } - }).toList(); - - return SizedBox( - width: 200, - child: ListView.separated( - shrinkWrap: true, - itemCount: items.length, - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - physics: StyledScrollPhysics(), - itemBuilder: (_, int index) => items[index], - padding: const EdgeInsets.all(6.0), - ), - ); - }, - ), - ); - } - - List _availableCalendarSettings( - CalendarLayoutSettingPB layoutSettings, - ) { - final List settings = [ - CalendarLayoutSettingAction.layoutField, - ]; - - switch (layoutSettings.layoutTy) { - case CalendarLayoutPB.DayLayout: - break; - case CalendarLayoutPB.MonthLayout: - case CalendarLayoutPB.WeekLayout: - settings.add(CalendarLayoutSettingAction.firstDayOfWeek); - break; - } - - return settings; - } -} - -class LayoutDateField extends StatelessWidget { - const LayoutDateField({ - super.key, - required this.databaseController, - required this.fieldId, - required this.popoverMutex, - required this.onUpdated, - }); - - final DatabaseController databaseController; - final String fieldId; - final PopoverMutex popoverMutex; - final Function(String fieldId) onUpdated; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.leftWithTopAligned, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - constraints: BoxConstraints.loose(const Size(300, 400)), - mutex: popoverMutex, - offset: const Offset(-14, 0), - popupBuilder: (context) { - return BlocProvider( - create: (context) => DatabasePropertyBloc( - viewId: databaseController.viewId, - fieldController: databaseController.fieldController, - )..add(const DatabasePropertyEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final items = state.fieldContexts - .where((field) => field.fieldType == FieldType.DateTime) - .map( - (fieldInfo) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - fieldInfo.name, - lineHeight: 1.0, - ), - onTap: () { - onUpdated(fieldInfo.id); - popoverMutex.close(); - }, - leftIcon: const FlowySvg(FlowySvgs.date_s), - rightIcon: fieldInfo.id == fieldId - ? const FlowySvg(FlowySvgs.check_s) - : null, - ), - ); - }, - ).toList(); - - return SizedBox( - width: 200, - child: ListView.separated( - shrinkWrap: true, - itemBuilder: (_, index) => items[index], - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: items.length, - ), - ); - }, - ), - ); - }, - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.calendar_settings_layoutDateField.tr(), - ), - ), - ), - ); - } -} - -class ShowWeekNumber extends StatelessWidget { - const ShowWeekNumber({ - super.key, - required this.showWeekNumbers, - required this.onUpdated, - }); - - final bool showWeekNumbers; - final Function(bool showWeekNumbers) onUpdated; - - @override - Widget build(BuildContext context) { - return _toggleItem( - onToggle: (showWeekNumbers) => onUpdated(!showWeekNumbers), - value: showWeekNumbers, - text: LocaleKeys.calendar_settings_showWeekNumbers.tr(), - ); - } -} - -class ShowWeekends extends StatelessWidget { - const ShowWeekends({ - super.key, - required this.showWeekends, - required this.onUpdated, - }); - - final bool showWeekends; - final Function(bool showWeekends) onUpdated; - - @override - Widget build(BuildContext context) { - return _toggleItem( - onToggle: (showWeekends) => onUpdated(!showWeekends), - value: showWeekends, - text: LocaleKeys.calendar_settings_showWeekends.tr(), - ); - } -} - -class FirstDayOfWeek extends StatelessWidget { - const FirstDayOfWeek({ - super.key, - required this.firstDayOfWeek, - required this.popoverMutex, - required this.onUpdated, - }); - - final int firstDayOfWeek; - final PopoverMutex popoverMutex; - final Function(int firstDayOfWeek) onUpdated; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.leftWithTopAligned, - constraints: BoxConstraints.loose(const Size(300, 400)), - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - mutex: popoverMutex, - offset: const Offset(-14, 0), - popupBuilder: (context) { - final symbols = - DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; - // starts from sunday - const len = 2; - final items = symbols.WEEKDAYS.take(len).indexed.map((entry) { - return StartFromButton( - title: entry.$2, - dayIndex: entry.$1, - isSelected: firstDayOfWeek == entry.$1, - onTap: (index) { - onUpdated(index); - popoverMutex.close(); - }, - ); - }).toList(); - - return SizedBox( - width: 100, - child: ListView.separated( - shrinkWrap: true, - itemBuilder: (_, index) => items[index], - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: len, - ), - ); - }, - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.calendar_settings_firstDayOfWeek.tr(), - ), - ), - ), - ); - } -} - -Widget _toggleItem({ - required String text, - required bool value, - required void Function(bool) onToggle, -}) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), - child: Row( - children: [ - FlowyText(text), - const Spacer(), - Toggle( - value: value, - onChanged: (value) => onToggle(value), - padding: EdgeInsets.zero, - ), - ], - ), - ), - ); -} - -enum CalendarLayoutSettingAction { - layoutField, - layoutType, - showWeekends, - firstDayOfWeek, - showWeekNumber, - showTimeLine, -} - -class StartFromButton extends StatelessWidget { - const StartFromButton({ - super.key, - required this.title, - required this.dayIndex, - required this.onTap, - required this.isSelected, - }); - - final String title; - final int dayIndex; - final void Function(int) onTap; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - title, - lineHeight: 1.0, - ), - 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 deleted file mode 100644 index 6bfe7b99a8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart +++ /dev/null @@ -1,61 +0,0 @@ -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, - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/cell_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/cell_listener.dart deleted file mode 100644 index 0b58300b97..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/cell_listener.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/notification.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'; - -import '../application/row/row_service.dart'; - -typedef UpdateFieldNotifiedValue = FlowyResult; - -class CellListener { - CellListener({required this.rowId, required this.fieldId}); - - final RowId rowId; - final String fieldId; - - PublishNotifier? _updateCellNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) { - _updateCellNotifier?.addPublishListener(onCellChanged); - _listener = DatabaseNotificationListener( - objectId: "$rowId:$fieldId", - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateCell: - result.fold( - (payload) => _updateCellNotifier?.value = FlowyResult.success(null), - (error) => _updateCellNotifier?.value = FlowyResult.failure(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _updateCellNotifier?.dispose(); - _updateCellNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/cell_service.dart deleted file mode 100644 index a4090d7c88..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/cell_service.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.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 '../application/cell/cell_controller.dart'; - -class CellBackendService { - CellBackendService(); - - static Future> updateCell({ - required String viewId, - required CellContext cellContext, - required String data, - }) { - final payload = CellChangesetPB() - ..viewId = viewId - ..fieldId = cellContext.fieldId - ..rowId = cellContext.rowId - ..cellChangeset = data; - return DatabaseEventUpdateCell(payload).send(); - } - - static Future> getCell({ - required String viewId, - required CellContext cellContext, - }) { - final payload = CellIdPB() - ..viewId = viewId - ..fieldId = cellContext.fieldId - ..rowId = cellContext.rowId; - return DatabaseEventGetCell(payload).send(); - } -} 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 deleted file mode 100644 index 3dcba2ca37..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/checklist_cell_service.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.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:protobuf/protobuf.dart'; - -class ChecklistCellBackendService { - ChecklistCellBackendService({ - required this.viewId, - required this.fieldId, - required this.rowId, - }); - - final String viewId; - final String fieldId; - final String rowId; - - 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); - - return DatabaseEventUpdateChecklistCell(payload).send(); - } - - Future> delete({ - required List optionIds, - }) { - final payload = ChecklistCellDataChangesetPB() - ..cellId = _makdeCellId() - ..deleteTasks.addAll(optionIds); - - return DatabaseEventUpdateChecklistCell(payload).send(); - } - - Future> select({ - required String optionId, - }) { - final payload = ChecklistCellDataChangesetPB() - ..cellId = _makdeCellId() - ..completedTasks.add(optionId); - - return DatabaseEventUpdateChecklistCell(payload).send(); - } - - Future> updateName({ - required SelectOptionPB option, - required name, - }) { - option.freeze(); - 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() - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/database_view_service.dart deleted file mode 100644 index de06c8d1d8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/database_view_service.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.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-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -import 'layout_service.dart'; - -class DatabaseViewBackendService { - DatabaseViewBackendService({required this.viewId}); - - final String viewId; - - /// Returns the database id associated with the view. - Future> getDatabaseId() async { - final payload = DatabaseViewIdPB(value: viewId); - return DatabaseEventGetDatabaseId(payload) - .send() - .then((value) => value.map((l) => l.value)); - } - - static Future> updateLayout({ - required String viewId, - required DatabaseLayoutPB layout, - }) { - final payload = UpdateViewPayloadPB.create() - ..viewId = viewId - ..layout = viewLayoutFromDatabaseLayout(layout); - - return FolderEventUpdateView(payload).send(); - } - - Future> openDatabase() async { - final payload = DatabaseViewIdPB(value: viewId); - return DatabaseEventGetDatabase(payload).send(); - } - - Future> moveGroupRow({ - required RowId fromRowId, - required String fromGroupId, - required String toGroupId, - RowId? toRowId, - }) { - final payload = MoveGroupRowPayloadPB.create() - ..viewId = viewId - ..fromRowId = fromRowId - ..fromGroupId = fromGroupId - ..toGroupId = toGroupId; - - if (toRowId != null) { - payload.toRowId = toRowId; - } - - return DatabaseEventMoveGroupRow(payload).send(); - } - - Future> moveRow({ - required String fromRowId, - required String toRowId, - }) { - final payload = MoveRowPayloadPB.create() - ..viewId = viewId - ..fromRowId = fromRowId - ..toRowId = toRowId; - - return DatabaseEventMoveRow(payload).send(); - } - - Future> moveGroup({ - required String fromGroupId, - required String toGroupId, - }) { - final payload = MoveGroupPayloadPB.create() - ..viewId = viewId - ..fromGroupId = fromGroupId - ..toGroupId = toGroupId; - - return DatabaseEventMoveGroup(payload).send(); - } - - Future, FlowyError>> getFields({ - List? fieldIds, - }) { - final payload = GetFieldPayloadPB.create()..viewId = viewId; - - if (fieldIds != null) { - payload.fieldIds = RepeatedFieldIdPB(items: fieldIds); - } - return DatabaseEventGetFields(payload).send().then((result) { - return result.fold( - (l) => FlowyResult.success(l.items), - (r) => FlowyResult.failure(r), - ); - }); - } - - Future> getLayoutSetting( - DatabaseLayoutPB layoutType, - ) { - final payload = DatabaseLayoutMetaPB.create() - ..viewId = viewId - ..layout = layoutType; - return DatabaseEventGetLayoutSetting(payload).send(); - } - - Future> updateLayoutSetting({ - required DatabaseLayoutPB layoutType, - BoardLayoutSettingPB? boardLayoutSetting, - CalendarLayoutSettingPB? calendarLayoutSetting, - }) { - final payload = LayoutSettingChangesetPB.create() - ..viewId = viewId - ..layoutType = layoutType; - - if (boardLayoutSetting != null) { - payload.board = boardLayoutSetting; - } - - if (calendarLayoutSetting != null) { - payload.calendar = calendarLayoutSetting; - } - - return DatabaseEventSetLayoutSetting(payload).send(); - } - - Future> closeView() { - final request = ViewIdPB(value: viewId); - return FolderEventCloseView(request).send(); - } - - Future> loadGroups() { - final payload = DatabaseViewIdPB(value: viewId); - return DatabaseEventGetGroups(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 deleted file mode 100644 index 4afd41ad9c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/date_cell_service.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:fixnum/fixnum.dart'; - -final class DateCellBackendService { - DateCellBackendService({ - required String viewId, - required String fieldId, - required String rowId, - }) : cellId = CellIdPB() - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId; - - final CellIdPB cellId; - - Future> update({ - bool? includeTime, - bool? isRange, - DateTime? date, - DateTime? endDate, - String? reminderId, - }) { - final payload = DateCellChangesetPB()..cellId = cellId; - - if (includeTime != null) { - payload.includeTime = includeTime; - } - if (isRange != null) { - payload.isRange = isRange; - } - if (date != null) { - final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000; - payload.timestamp = Int64(dateTimestamp); - } - if (endDate != null) { - final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000; - payload.endTimestamp = Int64(dateTimestamp); - } - if (reminderId != null) { - payload.reminderId = reminderId; - } - - return DatabaseEventUpdateDateCell(payload).send(); - } - - Future> clear() { - final payload = DateCellChangesetPB() - ..cellId = cellId - ..clearFlag = true; - - return DatabaseEventUpdateDateCell(payload).send(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_backend_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_backend_service.dart deleted file mode 100644 index 21933700ba..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_backend_service.dart +++ /dev/null @@ -1,79 +0,0 @@ -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/protobuf.dart'; - -// This class is used for combining the -// 1. FieldBackendService -// 2. FieldSettingsBackendService -// 3. TypeOptionBackendService -// -// including, -// hide, delete, duplicated, -// insertLeft, insertRight, -// updateName -class FieldServices { - FieldServices({ - required this.viewId, - required this.fieldId, - }) : fieldBackendService = FieldBackendService( - viewId: viewId, - fieldId: fieldId, - ), - fieldSettingsService = FieldSettingsBackendService( - viewId: viewId, - ); - - final String viewId; - final String fieldId; - - final FieldBackendService fieldBackendService; - final FieldSettingsBackendService fieldSettingsService; - - Future hide() async { - await fieldSettingsService.updateFieldSettings( - fieldId: fieldId, - fieldVisibility: FieldVisibility.AlwaysHidden, - ); - } - - Future show() async { - await fieldSettingsService.updateFieldSettings( - fieldId: fieldId, - fieldVisibility: FieldVisibility.AlwaysShown, - ); - } - - Future delete() async { - await fieldBackendService.delete(); - } - - Future duplicate() async { - await fieldBackendService.duplicate(); - } - - Future insertLeft() async { - await FieldBackendService.createField( - viewId: viewId, - position: OrderObjectPositionPB( - position: OrderObjectPositionTypePB.Before, - objectId: fieldId, - ), - ); - } - - Future insertRight() async { - await FieldBackendService.createField( - viewId: viewId, - position: OrderObjectPositionPB( - position: OrderObjectPositionTypePB.After, - objectId: fieldId, - ), - ); - } - - Future updateName(String name) async { - await fieldBackendService.updateField( - name: name, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_listener.dart deleted file mode 100644 index 4adbe7301b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_listener.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.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:flowy_infra/notifier.dart'; - -typedef UpdateFieldsNotifiedValue - = FlowyResult; - -class FieldsListener { - FieldsListener({required this.viewId}); - - final String viewId; - - PublishNotifier? updateFieldsNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(UpdateFieldsNotifiedValue) onFieldsChanged, - }) { - updateFieldsNotifier?.addPublishListener(onFieldsChanged); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateFields: - result.fold( - (payload) => updateFieldsNotifier?.value = - FlowyResult.success(DatabaseFieldChangesetPB.fromBuffer(payload)), - (error) => updateFieldsNotifier?.value = FlowyResult.failure(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - updateFieldsNotifier?.dispose(); - updateFieldsNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart deleted file mode 100644 index 66c941891d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy_backend/dispatch/dispatch.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'; - -/// FieldService provides many field-related interfaces event functions. Check out -/// `rust-lib/flowy-database/event_map.rs` for a list of events and their -/// implementations. -class FieldBackendService { - FieldBackendService({required this.viewId, required this.fieldId}); - - final String viewId; - final String fieldId; - - /// Create a field in a database view. The position will only be applicable - /// in this view; for other views it will be appended to the end - static Future> createField({ - required String viewId, - FieldType fieldType = FieldType.RichText, - String? fieldName, - String? icon, - Uint8List? typeOptionData, - OrderObjectPositionPB? position, - }) { - final payload = CreateFieldPayloadPB( - viewId: viewId, - fieldType: fieldType, - fieldName: fieldName, - typeOptionData: typeOptionData, - fieldPosition: position, - ); - - return DatabaseEventCreateField(payload).send(); - } - - /// Reorder a field within a database view - static Future> moveField({ - required String viewId, - required String fromFieldId, - required String toFieldId, - }) { - final payload = MoveFieldPayloadPB( - viewId: viewId, - fromFieldId: fromFieldId, - toFieldId: toFieldId, - ); - - return DatabaseEventMoveField(payload).send(); - } - - /// Delete a field - static Future> deleteField({ - required String viewId, - required String fieldId, - }) { - final payload = DeleteFieldPayloadPB( - viewId: viewId, - fieldId: fieldId, - ); - - return DatabaseEventDeleteField(payload).send(); - } - - // Clear all data of all cells in a Field - static Future> clearField({ - required String viewId, - required String fieldId, - }) { - final payload = ClearFieldPayloadPB( - viewId: viewId, - fieldId: fieldId, - ); - - return DatabaseEventClearField(payload).send(); - } - - /// Duplicate a field - static Future> duplicateField({ - required String viewId, - required String fieldId, - }) { - final payload = DuplicateFieldPayloadPB(viewId: viewId, fieldId: fieldId); - - return DatabaseEventDuplicateField(payload).send(); - } - - /// Update a field's properties - Future> updateField({ - String? name, - String? icon, - bool? frozen, - }) { - final payload = FieldChangesetPB.create() - ..viewId = viewId - ..fieldId = fieldId; - - if (name != null) { - payload.name = name; - } - - if (icon != null) { - payload.icon = icon; - } - - if (frozen != null) { - payload.frozen = frozen; - } - - return DatabaseEventUpdateField(payload).send(); - } - - /// Change a field's type - static Future> updateFieldType({ - 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(); - } - - /// Update a field's type option data - static Future> updateFieldTypeOption({ - required String viewId, - required String fieldId, - required List typeOptionData, - }) { - final payload = TypeOptionChangesetPB.create() - ..viewId = viewId - ..fieldId = fieldId - ..typeOptionData = typeOptionData; - - return DatabaseEventUpdateFieldTypeOption(payload).send(); - } - - /// Returns the primary field of the view. - static Future> getPrimaryField({ - required String viewId, - }) { - final payload = DatabaseViewIdPB.create()..value = viewId; - return DatabaseEventGetPrimaryField(payload).send(); - } - - Future> createBefore({ - FieldType fieldType = FieldType.RichText, - String? fieldName, - Uint8List? typeOptionData, - }) { - return createField( - viewId: viewId, - fieldType: fieldType, - fieldName: fieldName, - typeOptionData: typeOptionData, - position: OrderObjectPositionPB( - position: OrderObjectPositionTypePB.Before, - objectId: fieldId, - ), - ); - } - - Future> createAfter({ - FieldType fieldType = FieldType.RichText, - String? fieldName, - Uint8List? typeOptionData, - }) { - return createField( - viewId: viewId, - fieldType: fieldType, - fieldName: fieldName, - typeOptionData: typeOptionData, - position: OrderObjectPositionPB( - position: OrderObjectPositionTypePB.After, - objectId: fieldId, - ), - ); - } - - Future> updateType({ - required FieldType fieldType, - String? fieldName, - }) => - updateFieldType( - viewId: viewId, - fieldId: fieldId, - fieldType: fieldType, - fieldName: fieldName, - ); - - Future> delete() => - deleteField(viewId: viewId, fieldId: fieldId); - - Future> duplicate() => - duplicateField(viewId: viewId, fieldId: fieldId); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_listener.dart deleted file mode 100644 index 1313939091..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_listener.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.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:flowy_infra/notifier.dart'; - -typedef FieldSettingsValue = FlowyResult; - -class FieldSettingsListener { - FieldSettingsListener({required this.viewId}); - - final String viewId; - - PublishNotifier? _fieldSettingsNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(FieldSettingsValue) onFieldSettingsChanged, - }) { - _fieldSettingsNotifier?.addPublishListener(onFieldSettingsChanged); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateFieldSettings: - result.fold( - (payload) => _fieldSettingsNotifier?.value = - FlowyResult.success(FieldSettingsPB.fromBuffer(payload)), - (error) => _fieldSettingsNotifier?.value = FlowyResult.failure(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _fieldSettingsNotifier?.dispose(); - _fieldSettingsNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_service.dart deleted file mode 100644 index 0c36d1864e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_service.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.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'; - -class FieldSettingsBackendService { - FieldSettingsBackendService({required this.viewId}); - - final String viewId; - - Future> getFieldSettings( - String fieldId, - ) { - final id = FieldIdPB(fieldId: fieldId); - final ids = RepeatedFieldIdPB()..items.add(id); - final payload = FieldIdsPB() - ..viewId = viewId - ..fieldIds = ids; - - return DatabaseEventGetFieldSettings(payload).send().then((result) { - return result.fold( - (repeatedFieldSettings) { - final fieldSetting = repeatedFieldSettings.items.first; - if (!fieldSetting.hasVisibility()) { - fieldSetting.visibility = FieldVisibility.AlwaysShown; - } - - return FlowyResult.success(fieldSetting); - }, - (r) => FlowyResult.failure(r), - ); - }); - } - - Future, FlowyError>> getAllFieldSettings() { - final payload = DatabaseViewIdPB()..value = viewId; - - return DatabaseEventGetAllFieldSettings(payload).send().then((result) { - return result.fold( - (repeatedFieldSettings) { - final fieldSettings = []; - - for (final fieldSetting in repeatedFieldSettings.items) { - if (!fieldSetting.hasVisibility()) { - fieldSetting.visibility = FieldVisibility.AlwaysShown; - } - fieldSettings.add(fieldSetting); - } - - return FlowyResult.success(fieldSettings); - }, - (r) => FlowyResult.failure(r), - ); - }); - } - - Future> updateFieldSettings({ - required String fieldId, - FieldVisibility? fieldVisibility, - double? width, - bool? wrapCellContent, - }) { - final FieldSettingsChangesetPB payload = FieldSettingsChangesetPB.create() - ..viewId = viewId - ..fieldId = fieldId; - - if (fieldVisibility != null) { - payload.visibility = fieldVisibility; - } - - if (width != null) { - payload.width = width.round(); - } - - if (wrapCellContent != null) { - payload.wrapCellContent = wrapCellContent; - } - - return DatabaseEventUpdateFieldSettings(payload).send(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart deleted file mode 100644 index a9295e38dd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/util.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 UpdateFilterNotifiedValue - = FlowyResult; - -class FiltersListener { - FiltersListener({required this.viewId}); - - final String viewId; - - PublishNotifier? _filterNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(UpdateFilterNotifiedValue) onFilterChanged, - }) { - _filterNotifier?.addPublishListener(onFilterChanged); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateFilter: - result.fold( - (payload) => _filterNotifier?.value = FlowyResult.success( - FilterChangesetNotificationPB.fromBuffer(payload), - ), - (error) => _filterNotifier?.value = FlowyResult.failure(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _filterNotifier?.dispose(); - _filterNotifier = null; - } -} - -class FilterListener { - FilterListener({required this.viewId, required this.filterId}); - - final String viewId; - final String filterId; - - PublishNotifier? _onUpdateNotifier = PublishNotifier(); - - DatabaseNotificationListener? _listener; - - void start({void Function(FilterPB)? onUpdated}) { - _onUpdateNotifier?.addPublishListener((filter) { - onUpdated?.call(filter); - }); - - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void handleChangeset(FilterChangesetNotificationPB changeset) { - final filters = changeset.filters.items; - final updatedIndex = filters.indexWhere( - (filter) => filter.id == filterId, - ); - if (updatedIndex != -1) { - _onUpdateNotifier?.value = filters[updatedIndex]; - } - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateFilter: - result.fold( - (payload) => handleChangeset( - FilterChangesetNotificationPB.fromBuffer(payload), - ), - (error) {}, - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _onUpdateNotifier?.dispose(); - _onUpdateNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart deleted file mode 100644 index 4e191bf019..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart +++ /dev/null @@ -1,320 +0,0 @@ -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/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:fixnum/fixnum.dart' as $fixnum; - -class FilterBackendService { - const FilterBackendService({required this.viewId}); - - final String viewId; - - Future, FlowyError>> getAllFilters() { - final payload = DatabaseViewIdPB()..value = viewId; - - return DatabaseEventGetAllFilters(payload).send().then((result) { - return result.fold( - (repeated) => FlowyResult.success(repeated.items), - (r) => FlowyResult.failure(r), - ); - }); - } - - Future> insertTextFilter({ - required String fieldId, - String? filterId, - required TextFilterConditionPB condition, - required String content, - }) { - final filter = TextFilterPB() - ..condition = condition - ..content = content; - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: FieldType.RichText, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: FieldType.RichText, - data: filter.writeToBuffer(), - ); - } - - Future> insertCheckboxFilter({ - required String fieldId, - String? filterId, - required CheckboxFilterConditionPB condition, - }) { - final filter = CheckboxFilterPB()..condition = condition; - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: FieldType.Checkbox, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: FieldType.Checkbox, - data: filter.writeToBuffer(), - ); - } - - Future> insertNumberFilter({ - required String fieldId, - String? filterId, - required NumberFilterConditionPB condition, - String content = "", - }) { - final filter = NumberFilterPB() - ..condition = condition - ..content = content; - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: FieldType.Number, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: FieldType.Number, - data: filter.writeToBuffer(), - ); - } - - Future> insertDateFilter({ - required String fieldId, - required FieldType fieldType, - String? filterId, - required DateFilterConditionPB condition, - int? start, - int? end, - int? timestamp, - }) { - final filter = DateFilterPB()..condition = condition; - - if (timestamp != null) { - filter.timestamp = $fixnum.Int64(timestamp); - } - if (start != null) { - filter.start = $fixnum.Int64(start); - } - if (end != null) { - filter.end = $fixnum.Int64(end); - } - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: fieldType, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - data: filter.writeToBuffer(), - ); - } - - Future> insertURLFilter({ - required String fieldId, - String? filterId, - required TextFilterConditionPB condition, - String content = "", - }) { - final filter = TextFilterPB() - ..condition = condition - ..content = content; - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: FieldType.URL, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: FieldType.URL, - data: filter.writeToBuffer(), - ); - } - - Future> insertSelectOptionFilter({ - required String fieldId, - required FieldType fieldType, - required SelectOptionFilterConditionPB condition, - String? filterId, - List optionIds = const [], - }) { - final filter = SelectOptionFilterPB() - ..condition = condition - ..optionIds.addAll(optionIds); - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: fieldType, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - data: filter.writeToBuffer(), - ); - } - - Future> insertChecklistFilter({ - required String fieldId, - required ChecklistFilterConditionPB condition, - String? filterId, - List optionIds = const [], - }) { - final filter = ChecklistFilterPB()..condition = condition; - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: FieldType.Checklist, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: FieldType.Checklist, - data: filter.writeToBuffer(), - ); - } - - Future> insertTimeFilter({ - required String fieldId, - String? filterId, - required NumberFilterConditionPB condition, - String content = "", - }) { - final filter = TimeFilterPB() - ..condition = condition - ..content = content; - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: FieldType.Time, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: FieldType.Time, - data: filter.writeToBuffer(), - ); - } - - Future> insertFilter({ - required String fieldId, - required FieldType fieldType, - required List data, - }) async { - final filterData = FilterDataPB() - ..fieldId = fieldId - ..fieldType = fieldType - ..data = data; - - final insertFilterPayload = InsertFilterPB()..data = filterData; - - final payload = DatabaseSettingChangesetPB() - ..viewId = viewId - ..insertFilter = insertFilterPayload; - - final result = await DatabaseEventUpdateDatabaseSetting(payload).send(); - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - } - - Future> updateFilter({ - required String filterId, - required String fieldId, - required FieldType fieldType, - required List data, - }) async { - final filterData = FilterDataPB() - ..fieldId = fieldId - ..fieldType = fieldType - ..data = data; - - final updateFilterPayload = UpdateFilterDataPB() - ..filterId = filterId - ..data = filterData; - - final payload = DatabaseSettingChangesetPB() - ..viewId = viewId - ..updateFilterData = updateFilterPayload; - - final result = await DatabaseEventUpdateDatabaseSetting(payload).send(); - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - } - - 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 filterId, - }) async { - final deleteFilterPayload = DeleteFilterPB()..filterId = filterId; - - final payload = DatabaseSettingChangesetPB() - ..viewId = viewId - ..deleteFilter = deleteFilterPayload; - - final result = await DatabaseEventUpdateDatabaseSetting(payload).send(); - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart deleted file mode 100644 index 8720c18866..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/notification.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 GroupUpdateValue = FlowyResult; -typedef GroupByNewFieldValue = FlowyResult, FlowyError>; - -class DatabaseGroupListener { - DatabaseGroupListener(this.viewId); - - final String viewId; - - PublishNotifier? _numOfGroupsNotifier = PublishNotifier(); - PublishNotifier? _groupByFieldNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(GroupUpdateValue) onNumOfGroupsChanged, - required void Function(GroupByNewFieldValue) onGroupByNewField, - }) { - _numOfGroupsNotifier?.addPublishListener(onNumOfGroupsChanged); - _groupByFieldNotifier?.addPublishListener(onGroupByNewField); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateNumOfGroups: - result.fold( - (payload) => _numOfGroupsNotifier?.value = - FlowyResult.success(GroupChangesPB.fromBuffer(payload)), - (error) => _numOfGroupsNotifier?.value = FlowyResult.failure(error), - ); - break; - case DatabaseNotification.DidGroupByField: - result.fold( - (payload) => _groupByFieldNotifier?.value = FlowyResult.success( - GroupChangesPB.fromBuffer(payload).initialGroups, - ), - (error) => _groupByFieldNotifier?.value = FlowyResult.failure(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _numOfGroupsNotifier?.dispose(); - _numOfGroupsNotifier = null; - - _groupByFieldNotifier?.dispose(); - _groupByFieldNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart deleted file mode 100644 index f7a9be4a9c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.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'; - -class GroupBackendService { - GroupBackendService(this.viewId); - - final String viewId; - - Future> groupByField({ - required String fieldId, - required List settingContent, - }) { - final payload = GroupByFieldPayloadPB.create() - ..viewId = viewId - ..fieldId = fieldId - ..settingContent = settingContent; - - return DatabaseEventSetGroupByField(payload).send(); - } - - Future> updateGroup({ - required String groupId, - String? name, - bool? visible, - }) { - final payload = UpdateGroupPB.create() - ..viewId = viewId - ..groupId = groupId; - - if (name != null) { - payload.name = name; - } - if (visible != null) { - payload.visible = visible; - } - return DatabaseEventUpdateGroup(payload).send(); - } - - Future> createGroup({ - required String name, - String groupConfigId = "", - }) { - final payload = CreateGroupPayloadPB.create() - ..viewId = viewId - ..name = name; - - return DatabaseEventCreateGroup(payload).send(); - } - - Future> deleteGroup({ - required String groupId, - }) { - final payload = DeleteGroupPayloadPB.create() - ..viewId = viewId - ..groupId = groupId; - - return DatabaseEventDeleteGroup(payload).send(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart deleted file mode 100644 index 58f06d06a8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; - -ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) { - switch (databaseLayout) { - case DatabaseLayoutPB.Board: - return ViewLayoutPB.Board; - case DatabaseLayoutPB.Calendar: - return ViewLayoutPB.Calendar; - case DatabaseLayoutPB.Grid: - return ViewLayoutPB.Grid; - default: - throw UnimplementedError; - } -} - -DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) { - switch (viewLayout) { - case ViewLayoutPB.Board: - return DatabaseLayoutPB.Board; - case ViewLayoutPB.Calendar: - return DatabaseLayoutPB.Calendar; - case ViewLayoutPB.Grid: - return DatabaseLayoutPB.Grid; - default: - throw UnimplementedError; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/row_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/row_listener.dart deleted file mode 100644 index 3d033128a8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/row_listener.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.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_result/appflowy_result.dart'; - -typedef DidFetchRowCallback = void Function(DidFetchRowPB); -typedef RowMetaCallback = void Function(RowMetaPB); - -class RowListener { - RowListener(this.rowId); - - final String rowId; - - DidFetchRowCallback? _onRowFetchedCallback; - RowMetaCallback? _onMetaChangedCallback; - DatabaseNotificationListener? _listener; - - /// OnMetaChanged will be called when the row meta is changed. - /// OnRowFetched will be called when the row is fetched from remote storage - void start({ - RowMetaCallback? onMetaChanged, - DidFetchRowCallback? onRowFetched, - }) { - _onMetaChangedCallback = onMetaChanged; - _onRowFetchedCallback = onRowFetched; - _listener = DatabaseNotificationListener( - objectId: rowId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateRowMeta: - result.fold( - (payload) { - if (_onMetaChangedCallback != null) { - _onMetaChangedCallback!(RowMetaPB.fromBuffer(payload)); - } - }, - (error) => Log.error(error), - ); - break; - case DatabaseNotification.DidFetchRow: - result.fold( - (payload) { - if (_onRowFetchedCallback != null) { - _onRowFetchedCallback!(DidFetchRowPB.fromBuffer(payload)); - } - }, - (error) => Log.error(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _onMetaChangedCallback = null; - _onRowFetchedCallback = null; - } -} 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 deleted file mode 100644 index 2e0c24718e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.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:nanoid/nanoid.dart'; - -class SelectOptionCellBackendService { - SelectOptionCellBackendService({ - required this.viewId, - required this.fieldId, - required this.rowId, - }); - - final String viewId; - final String fieldId; - final String rowId; - - Future> create({ - required String name, - SelectOptionColorPB? color, - bool isSelected = true, - }) { - final option = SelectOptionPB() - ..id = nanoid(4) - ..name = name; - if (color != null) { - option.color = color; - } - - final payload = RepeatedSelectOptionPayload() - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId - ..items.add(option); - - return DatabaseEventInsertOrUpdateSelectOption(payload).send(); - } - - Future> update({ - required SelectOptionPB option, - }) { - final payload = RepeatedSelectOptionPayload() - ..items.add(option) - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId; - - return DatabaseEventInsertOrUpdateSelectOption(payload).send(); - } - - Future> delete({ - required Iterable options, - }) { - final payload = RepeatedSelectOptionPayload() - ..items.addAll(options) - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId; - - return DatabaseEventDeleteSelectOption(payload).send(); - } - - Future> select({ - required Iterable optionIds, - }) { - final payload = SelectOptionCellChangesetPB() - ..cellIdentifier = _cellIdentifier() - ..insertOptionIds.addAll(optionIds); - - return DatabaseEventUpdateSelectOptionCell(payload).send(); - } - - Future> unselect({ - required Iterable optionIds, - }) { - final payload = SelectOptionCellChangesetPB() - ..cellIdentifier = _cellIdentifier() - ..deleteOptionIds.addAll(optionIds); - - return DatabaseEventUpdateSelectOptionCell(payload).send(); - } - - CellIdPB _cellIdentifier() { - return CellIdPB() - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/sort_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/sort_listener.dart deleted file mode 100644 index 83c9310331..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/sort_listener.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/sort_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 SortNotifiedValue - = FlowyResult; - -class SortsListener { - SortsListener({required this.viewId}); - - final String viewId; - - PublishNotifier? _notifier = PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(SortNotifiedValue) onSortChanged, - }) { - _notifier?.addPublishListener(onSortChanged); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateSort: - result.fold( - (payload) => _notifier?.value = FlowyResult.success( - SortChangesetNotificationPB.fromBuffer(payload), - ), - (error) => _notifier?.value = FlowyResult.failure(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _notifier?.dispose(); - _notifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart deleted file mode 100644 index bdd8ea9716..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -class SortBackendService { - SortBackendService({required this.viewId}); - - final String viewId; - - Future, FlowyError>> getAllSorts() { - final payload = DatabaseViewIdPB()..value = viewId; - - return DatabaseEventGetAllSorts(payload).send().then((result) { - return result.fold( - (repeated) => FlowyResult.success(repeated.items), - (r) => FlowyResult.failure(r), - ); - }); - } - - Future> updateSort({ - required String sortId, - required String fieldId, - required SortConditionPB condition, - }) { - final insertSortPayload = UpdateSortPayloadPB.create() - ..viewId = viewId - ..sortId = sortId - ..fieldId = fieldId - ..condition = condition; - - final payload = DatabaseSettingChangesetPB.create() - ..viewId = viewId - ..updateSort = insertSortPayload; - return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - }); - } - - Future> insertSort({ - required String fieldId, - required SortConditionPB condition, - }) { - final insertSortPayload = UpdateSortPayloadPB.create() - ..fieldId = fieldId - ..viewId = viewId - ..condition = condition; - - final payload = DatabaseSettingChangesetPB.create() - ..viewId = viewId - ..updateSort = insertSortPayload; - return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - }); - } - - Future> reorderSort({ - required String fromSortId, - required String toSortId, - }) { - final payload = DatabaseSettingChangesetPB() - ..viewId = viewId - ..reorderSort = (ReorderSortPayloadPB() - ..viewId = viewId - ..fromSortId = fromSortId - ..toSortId = toSortId); - - return DatabaseEventUpdateDatabaseSetting(payload).send(); - } - - Future> deleteSort({ - required String sortId, - }) { - final deleteSortPayload = DeleteSortPayloadPB.create() - ..sortId = sortId - ..viewId = viewId; - - final payload = DatabaseSettingChangesetPB.create() - ..viewId = viewId - ..deleteSort = deleteSortPayload; - - return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - }); - } - - Future> deleteAllSorts() { - final payload = DatabaseViewIdPB(value: viewId); - return DatabaseEventDeleteAllSorts(payload).send().then((result) { - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/type_option_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/type_option_service.dart deleted file mode 100644 index a222e69853..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/type_option_service.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.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'; - -class TypeOptionBackendService { - TypeOptionBackendService({ - required this.viewId, - required this.fieldId, - }); - - final String viewId; - final String fieldId; - - Future> newOption({ - required String name, - }) { - final payload = CreateSelectOptionPayloadPB.create() - ..optionName = name - ..viewId = viewId - ..fieldId = fieldId; - - return DatabaseEventCreateSelectOption(payload).send(); - } -} 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 deleted file mode 100644 index a2b80a29df..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:appflowy/plugins/database/application/calculations/calculations_listener.dart'; -import 'package:appflowy/plugins/database/application/calculations/calculations_service.dart'; -import 'package:appflowy/plugins/database/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'calculations_bloc.freezed.dart'; - -class CalculationsBloc extends Bloc { - CalculationsBloc({ - required this.viewId, - required FieldController fieldController, - }) : _fieldController = fieldController, - _calculationsListener = CalculationsListener(viewId: viewId), - _calculationsService = CalculationsBackendService(viewId: viewId), - super(CalculationsState.initial()) { - _dispatch(); - } - - final String viewId; - final FieldController _fieldController; - final CalculationsListener _calculationsListener; - late final CalculationsBackendService _calculationsService; - - @override - Future close() async { - _fieldController.removeListener(onFieldsListener: _onReceiveFields); - await _calculationsListener.stop(); - await super.close(); - } - - void _dispatch() { - on((event, emit) async { - await event.when( - started: () async { - _startListening(); - await _getAllCalculations(); - - if (!isClosed) { - add( - CalculationsEvent.didReceiveFieldUpdate( - _fieldController.fieldInfos, - ), - ); - } - }, - didReceiveFieldUpdate: (fields) async { - emit( - state.copyWith( - fields: fields - .where( - (e) => - e.visibility != null && - e.visibility != FieldVisibility.AlwaysHidden, - ) - .toList(), - ), - ); - }, - didReceiveCalculationsUpdate: (calculationsMap) async { - emit( - state.copyWith( - calculationsByFieldId: calculationsMap, - ), - ); - }, - updateCalculationType: (fieldId, type, calculationId) async { - await _calculationsService.updateCalculation( - fieldId, - type, - calculationId: calculationId, - ); - }, - removeCalculation: (fieldId, calculationId) async { - await _calculationsService.removeCalculation(fieldId, calculationId); - }, - ); - }); - } - - void _startListening() { - _fieldController.addListener( - listenWhen: () => !isClosed, - onReceiveFields: _onReceiveFields, - ); - - _calculationsListener.start( - onCalculationChanged: (changesetOrFailure) { - if (isClosed) { - return; - } - - changesetOrFailure.fold( - (changeset) { - final calculationsMap = {...state.calculationsByFieldId}; - if (changeset.insertCalculations.isNotEmpty) { - for (final insert in changeset.insertCalculations) { - calculationsMap[insert.fieldId] = insert; - } - } - - if (changeset.updateCalculations.isNotEmpty) { - for (final update in changeset.updateCalculations) { - calculationsMap.removeWhere((key, _) => key == update.fieldId); - calculationsMap.addAll({update.fieldId: update}); - } - } - - if (changeset.deleteCalculations.isNotEmpty) { - for (final delete in changeset.deleteCalculations) { - calculationsMap.removeWhere((key, _) => key == delete.fieldId); - } - } - - add( - CalculationsEvent.didReceiveCalculationsUpdate( - calculationsMap, - ), - ); - }, - (_) => null, - ); - }, - ); - } - - void _onReceiveFields(List fields) => - add(CalculationsEvent.didReceiveFieldUpdate(fields)); - - Future _getAllCalculations() async { - final calculationsOrFailure = await _calculationsService.getCalculations(); - - if (isClosed) { - return; - } - - final RepeatedCalculationsPB? calculations = - calculationsOrFailure.fold((s) => s, (e) => null); - if (calculations != null) { - final calculationMap = {}; - for (final calculation in calculations.items) { - calculationMap[calculation.fieldId] = calculation; - } - - add(CalculationsEvent.didReceiveCalculationsUpdate(calculationMap)); - } - } -} - -@freezed -class CalculationsEvent with _$CalculationsEvent { - const factory CalculationsEvent.started() = _Started; - - const factory CalculationsEvent.didReceiveFieldUpdate( - List fields, - ) = _DidReceiveFieldUpdate; - - const factory CalculationsEvent.didReceiveCalculationsUpdate( - Map calculationsByFieldId, - ) = _DidReceiveCalculationsUpdate; - - const factory CalculationsEvent.updateCalculationType( - String fieldId, - CalculationType type, { - @Default(null) String? calculationId, - }) = _UpdateCalculationType; - - const factory CalculationsEvent.removeCalculation( - String fieldId, - String calculationId, - ) = _RemoveCalculation; -} - -@freezed -class CalculationsState with _$CalculationsState { - const factory CalculationsState({ - required List fields, - required Map calculationsByFieldId, - }) = _CalculationsState; - - factory CalculationsState.initial() => const CalculationsState( - fields: [], - calculationsByFieldId: {}, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart deleted file mode 100644 index 8fbb40ffde..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -extension AvailableCalculations on FieldType { - List calculationsForFieldType() { - final calculationTypes = [ - CalculationType.Count, - ]; - - // These FieldTypes cannot be empty, or might hold secondary - // data causing them to be seen as not empty when in fact they - // are empty. - if (![ - FieldType.URL, - FieldType.Checkbox, - FieldType.LastEditedTime, - FieldType.CreatedTime, - ].contains(this)) { - calculationTypes.addAll([ - CalculationType.CountEmpty, - CalculationType.CountNonEmpty, - ]); - } - - switch (this) { - case FieldType.Number: - calculationTypes.addAll([ - CalculationType.Sum, - CalculationType.Average, - CalculationType.Min, - CalculationType.Max, - CalculationType.Median, - ]); - break; - default: - break; - } - - return calculationTypes; - } -} 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/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/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_bloc.dart deleted file mode 100644 index 9b59b997b1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_bloc.dart +++ /dev/null @@ -1,267 +0,0 @@ -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_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'; - -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)) { - _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, - ); - result.fold( - (createdRow) => emit( - state.copyWith( - createdRow: createdRow, - openRowDetail: openRowDetail ?? false, - visibleRows: state.visibleRows + 1, - ), - ), - (err) => Log.error(err), - ); - }, - resetCreatedRow: () { - emit(state.copyWith(createdRow: null, openRowDetail: false)); - }, - deleteRow: (rowInfo) async { - await RowBackendService.deleteRows(viewId, [rowInfo.rowId]); - }, - moveRow: (int from, int to) { - final List rows = [...state.rowInfos]; - - final fromRow = rows[from].rowId; - final toRow = rows[to].rowId; - - rows.insert(to, rows.removeAt(from)); - emit(state.copyWith(rowInfos: rows)); - - databaseController.moveRow(fromRowId: fromRow, toRowId: toRow); - }, - didReceiveFieldUpdate: (fields) { - emit(state.copyWith(fields: fields)); - }, - didLoadRows: (newRowInfos, reason) { - emit( - state.copyWith( - rowInfos: newRowInfos, - rowCount: newRowInfos.length, - reason: reason, - ), - ); - }, - didReceveFilters: (filters) { - emit(state.copyWith(filters: filters)); - }, - didReceveSorts: (sorts) { - emit(state.copyWith(reorderable: sorts.isEmpty, sorts: sorts)); - }, - loadMoreRows: () { - emit(state.copyWith(visibleRows: state.visibleRows + 25)); - }, - ); - }, - ); - } - - RowCache get rowCache => databaseController.rowCache; - - void _startListening() { - _databaseCallbacks = DatabaseCallbacks( - 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), - ); - } - }, - onFieldsChanged: (fields) { - if (!isClosed) { - add(GridEvent.didReceiveFieldUpdate(fields)); - } - }, - onFiltersChanged: (filters) { - if (!isClosed) { - add(GridEvent.didReceveFilters(filters)); - } - }, - onSortsChanged: (sorts) { - if (!isClosed) { - add(GridEvent.didReceveSorts(sorts)); - } - }, - ); - databaseController.addListener(onDatabaseChanged: _databaseCallbacks); - } - - Future _openGrid(Emitter emit) async { - final result = await databaseController.open(); - result.fold( - (grid) { - databaseController.setIsLoading(false); - emit( - state.copyWith( - loadingState: LoadingState.finish(FlowyResult.success(null)), - ), - ); - }, - (err) => emit( - state.copyWith( - loadingState: LoadingState.finish(FlowyResult.failure(err)), - ), - ), - ); - } -} - -@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; - const factory GridEvent.moveRow(int from, int to) = _MoveRow; - const factory GridEvent.didLoadRows( - List rows, - ChangedReason reason, - ) = _DidReceiveRowUpdate; - const factory GridEvent.didReceiveFieldUpdate( - List fields, - ) = _DidReceiveFieldUpdate; - const factory GridEvent.didReceveFilters(List filters) = - _DidReceiveFilters; - const factory GridEvent.didReceveSorts(List sorts) = - _DidReceiveSorts; - const factory GridEvent.loadMoreRows() = _LoadMoreRows; -} - -@freezed -class GridState with _$GridState { - const factory GridState({ - required String viewId, - required List fields, - required List rowInfos, - required int rowCount, - required RowMetaPB? createdRow, - required LoadingState loadingState, - required bool reorderable, - required ChangedReason reason, - required List sorts, - required List filters, - required bool openRowDetail, - @Default(0) int visibleRows, - }) = _GridState; - - factory GridState.initial(String viewId) => GridState( - fields: [], - rowInfos: [], - rowCount: 0, - createdRow: null, - viewId: viewId, - reorderable: true, - loadingState: const LoadingState.loading(), - reason: const InitialListState(), - 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 deleted file mode 100644 index a869b636d2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_header_bloc.dart +++ /dev/null @@ -1,126 +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_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../domain/field_service.dart'; - -part 'grid_header_bloc.freezed.dart'; - -class GridHeaderBloc extends Bloc { - GridHeaderBloc({required this.viewId, required this.fieldController}) - : super(GridHeaderState.initial()) { - _dispatch(); - } - - final String viewId; - final FieldController fieldController; - - @override - Future close() async { - fieldController.removeListener(onFieldsListener: _onReceiveFields); - await super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () { - _startListening(); - add( - GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos), - ); - }, - didReceiveFieldUpdate: (List fields) { - emit( - state.copyWith( - fields: fields - .where( - (element) => - element.visibility != null && - element.visibility != FieldVisibility.AlwaysHidden, - ) - .toList(), - ), - ); - }, - startEditingField: (fieldId) { - emit(state.copyWith(editingFieldId: fieldId)); - }, - startEditingNewField: (fieldId) { - emit(state.copyWith(editingFieldId: fieldId, newFieldId: fieldId)); - }, - endEditingField: () { - emit(state.copyWith(editingFieldId: null, newFieldId: null)); - }, - moveField: (fromIndex, toIndex) async { - await _moveField(fromIndex, toIndex, emit); - }, - ); - }, - ); - } - - Future _moveField( - int fromIndex, - int toIndex, - Emitter emit, - ) async { - final fromId = state.fields[fromIndex].id; - final toId = state.fields[toIndex].id; - - final fields = List.from(state.fields); - fields.insert(toIndex, fields.removeAt(fromIndex)); - emit(state.copyWith(fields: fields)); - - final result = await FieldBackendService.moveField( - viewId: viewId, - fromFieldId: fromId, - toFieldId: toId, - ); - result.fold((l) {}, (err) => Log.error(err)); - } - - void _startListening() { - fieldController.addListener( - onReceiveFields: _onReceiveFields, - listenWhen: () => !isClosed, - ); - } - - void _onReceiveFields(List fields) => - add(GridHeaderEvent.didReceiveFieldUpdate(fields)); -} - -@freezed -class GridHeaderEvent with _$GridHeaderEvent { - const factory GridHeaderEvent.initial() = _InitialHeader; - const factory GridHeaderEvent.didReceiveFieldUpdate(List fields) = - _DidReceiveFieldUpdate; - const factory GridHeaderEvent.startEditingField(String fieldId) = - _StartEditingField; - const factory GridHeaderEvent.startEditingNewField(String fieldId) = - _StartEditingNewField; - const factory GridHeaderEvent.endEditingField() = _EndEditingField; - const factory GridHeaderEvent.moveField( - int fromIndex, - int toIndex, - ) = _MoveField; -} - -@freezed -class GridHeaderState with _$GridHeaderState { - const factory GridHeaderState({ - required List fields, - required String? editingFieldId, - required String? newFieldId, - }) = _GridHeaderState; - - factory GridHeaderState.initial() => - const GridHeaderState(fields: [], editingFieldId: null, newFieldId: null); -} 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 deleted file mode 100644 index ec42cd5cac..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/mobile_row_detail_bloc.dart +++ /dev/null @@ -1,123 +0,0 @@ -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'; - -part 'mobile_row_detail_bloc.freezed.dart'; - -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 { - _startListening(); - - emit( - state.copyWith( - isLoading: false, - currentRowId: rowId, - 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)); - }, - 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( - onNumOfRowsChanged: (rowInfos, _, reason) { - if (!isClosed) { - add(MobileRowDetailEvent.didLoadRows(rowInfos)); - } - }, - onRowsUpdated: (rows, reason) { - if (!isClosed) { - add( - MobileRowDetailEvent.didLoadRows( - databaseController.rowCache.rowInfos, - ), - ); - } - }, - ); - databaseController.addListener(onDatabaseChanged: _databaseCallbacks); - } -} - -@freezed -class MobileRowDetailEvent with _$MobileRowDetailEvent { - const factory MobileRowDetailEvent.initial(String rowId) = _Initial; - const factory MobileRowDetailEvent.didLoadRows(List rows) = - _DidLoadRows; - const factory MobileRowDetailEvent.changeRowId(String rowId) = _ChangeRowId; - const factory MobileRowDetailEvent.addCover(RowCoverPB cover) = _AddCover; -} - -@freezed -class MobileRowDetailState with _$MobileRowDetailState { - const factory MobileRowDetailState({ - required bool isLoading, - required String? currentRowId, - required List rowInfos, - }) = _MobileRowDetailState; - - factory MobileRowDetailState.initial() { - return const MobileRowDetailState( - isLoading: true, - rowInfos: [], - currentRowId: null, - ); - } -} 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 deleted file mode 100644 index 402ae6e596..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:async'; - -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/widgets/setting/field_visibility_extension.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../../application/row/row_cache.dart'; -import '../../../application/row/row_controller.dart'; -import '../../../application/row/row_service.dart'; - -part 'row_bloc.freezed.dart'; - -class RowBloc extends Bloc { - RowBloc({ - required this.fieldController, - required this.rowId, - required this.viewId, - required RowController rowController, - }) : _rowBackendSvc = RowBackendService(viewId: viewId), - _rowController = rowController, - super(RowState.initial()) { - _dispatch(); - _startListening(); - _init(); - rowController.initialize(); - } - - final FieldController fieldController; - final RowBackendService _rowBackendSvc; - final RowController _rowController; - final String viewId; - final String rowId; - - @override - Future close() async { - await _rowController.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - event.when( - createRow: () { - _rowBackendSvc.createRowAfter(rowId); - }, - didReceiveCells: (List cellContexts, reason) { - final visibleCellContexts = cellContexts - .where( - (cellContext) => fieldController - .getField(cellContext.fieldId)! - .fieldSettings! - .visibility - .isVisibleState(), - ) - .toList(); - emit( - state.copyWith( - cellContexts: visibleCellContexts, - changeReason: reason, - ), - ); - }, - ); - }, - ); - } - - void _startListening() => - _rowController.addListener(onRowChanged: _onRowChanged); - - void _onRowChanged(List cells, ChangedReason reason) { - if (!isClosed) { - add(RowEvent.didReceiveCells(cells, reason)); - } - } - - void _init() { - add( - RowEvent.didReceiveCells( - _rowController.loadCells(), - const ChangedReason.setInitialRows(), - ), - ); - } -} - -@freezed -class RowEvent with _$RowEvent { - const factory RowEvent.createRow() = _CreateRow; - const factory RowEvent.didReceiveCells( - List cellsByFieldId, - ChangedReason reason, - ) = _DidReceiveCells; -} - -@freezed -class RowState with _$RowState { - const factory RowState({ - required List cellContexts, - ChangedReason? changeReason, - }) = _RowState; - - factory RowState.initial() => const RowState(cellContexts: []); -} 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 deleted file mode 100644 index e7d4658df8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart +++ /dev/null @@ -1,294 +0,0 @@ -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/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'; - -part 'row_detail_bloc.freezed.dart'; - -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)) { - _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(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didReceiveCellDatas: (visibleCells, numHiddenFields) { - emit( - state.copyWith( - visibleCells: visibleCells, - numHiddenFields: numHiddenFields, - ), - ); - }, - didUpdateFields: (fields) { - emit(state.copyWith(fields: fields)); - }, - deleteField: (fieldId) async { - final result = await FieldBackendService.deleteField( - viewId: rowController.viewId, - fieldId: fieldId, - ); - result.fold((l) {}, (err) => Log.error(err)); - }, - toggleFieldVisibility: (fieldId) async { - await _toggleFieldVisibility(fieldId, emit); - }, - reorderField: (fromIndex, toIndex) async { - await _reorderField(fromIndex, toIndex, emit); - }, - toggleHiddenFieldVisibility: () { - final showHiddenFields = !state.showHiddenFields; - final visibleCells = List.from( - allCells.where((cellContext) { - final fieldInfo = fieldController.getField(cellContext.fieldId); - return fieldInfo != null && - !fieldInfo.isPrimary && - (fieldInfo.visibility!.isVisibleState() || - showHiddenFields); - }), - ); - - emit( - state.copyWith( - showHiddenFields: showHiddenFields, - visibleCells: visibleCells, - ), - ); - }, - 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) { - return; - } - allCells.clear(); - allCells.addAll(cellMap); - int numHiddenFields = 0; - final visibleCells = []; - - for (final cellContext in allCells) { - final fieldInfo = fieldController.getField(cellContext.fieldId); - if (fieldInfo == null || fieldInfo.isPrimary) { - continue; - } - final isHidden = !fieldInfo.visibility!.isVisibleState(); - if (!isHidden || state.showHiddenFields) { - visibleCells.add(cellContext); - } - if (isHidden) { - numHiddenFields++; - } - } - - add( - RowDetailEvent.didReceiveCellDatas( - visibleCells, - numHiddenFields, - ), - ); - }, - ); - fieldController.addListener( - onReceiveFields: (fields) => add(RowDetailEvent.didUpdateFields(fields)), - listenWhen: () => !isClosed, - ); - } - - void _init() { - allCells.addAll(rowController.loadCells()); - int numHiddenFields = 0; - final visibleCells = []; - for (final cell in allCells) { - final fieldInfo = fieldController.getField(cell.fieldId); - if (fieldInfo == null || fieldInfo.isPrimary) { - continue; - } - final isHidden = !fieldInfo.visibility!.isVisibleState(); - if (!isHidden) { - visibleCells.add(cell); - } else { - numHiddenFields++; - } - } - add( - RowDetailEvent.didReceiveCellDatas( - visibleCells, - numHiddenFields, - ), - ); - add(RowDetailEvent.didUpdateFields(fieldController.fieldInfos)); - } - - Future _toggleFieldVisibility( - String fieldId, - Emitter emit, - ) async { - final fieldInfo = fieldController.getField(fieldId)!; - final fieldVisibility = fieldInfo.visibility == FieldVisibility.AlwaysShown - ? FieldVisibility.AlwaysHidden - : FieldVisibility.AlwaysShown; - final result = - await FieldSettingsBackendService(viewId: rowController.viewId) - .updateFieldSettings( - fieldId: fieldId, - fieldVisibility: fieldVisibility, - ); - result.fold((l) {}, (err) => Log.error(err)); - } - - Future _reorderField( - int fromIndex, - int toIndex, - Emitter emit, - ) async { - if (fromIndex < toIndex) { - toIndex--; - } - final fromId = state.visibleCells[fromIndex].fieldId; - final toId = state.visibleCells[toIndex].fieldId; - - final cells = List.from(state.visibleCells); - cells.insert(toIndex, cells.removeAt(fromIndex)); - emit(state.copyWith(visibleCells: cells)); - - final result = await FieldBackendService.moveField( - viewId: rowController.viewId, - fromFieldId: fromId, - toFieldId: toId, - ); - result.fold((l) {}, (err) => Log.error(err)); - } -} - -@freezed -class RowDetailEvent with _$RowDetailEvent { - const factory RowDetailEvent.didUpdateFields(List fields) = - _DidUpdateFields; - - /// Triggered by listeners to update row data - const factory RowDetailEvent.didReceiveCellDatas( - List visibleCells, - int numHiddenFields, - ) = _DidReceiveCellDatas; - - /// Used to delete a field - const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField; - - /// Used to show/hide a field - const factory RowDetailEvent.toggleFieldVisibility(String fieldId) = - _ToggleFieldVisibility; - - /// Used to reorder a field - const factory RowDetailEvent.reorderField( - int fromIndex, - int toIndex, - ) = _ReorderField; - - /// 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 -class RowDetailState with _$RowDetailState { - const factory RowDetailState({ - required List fields, - 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( - fields: [], - visibleCells: [], - showHiddenFields: false, - numHiddenFields: 0, - editingFieldId: "", - newFieldId: "", - rowMeta: rowMeta, - ); -} 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 deleted file mode 100644 index 37b0e37747..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart +++ /dev/null @@ -1,187 +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/sort_entities.dart'; -import 'package:appflowy/plugins/database/domain/sort_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:collection/collection.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'sort_editor_bloc.freezed.dart'; - -class SortEditorBloc extends Bloc { - SortEditorBloc({ - required this.viewId, - required this.fieldController, - }) : _sortBackendSvc = SortBackendService(viewId: viewId), - super( - SortEditorState.initial( - fieldController.sorts, - fieldController.fieldInfos, - ), - ) { - _dispatch(); - _startListening(); - } - - final String viewId; - final SortBackendService _sortBackendSvc; - final FieldController fieldController; - - void Function(List)? _onFieldFn; - void Function(List)? _onSortsFn; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didReceiveFields: (List fields) { - emit( - state.copyWith( - allFields: fields, - creatableFields: _getCreatableSorts(fields), - ), - ); - }, - createSort: ( - String fieldId, - SortConditionPB? condition, - ) async { - final result = await _sortBackendSvc.insertSort( - fieldId: fieldId, - condition: condition ?? SortConditionPB.Ascending, - ); - result.fold((l) => {}, (err) => Log.error(err)); - }, - editSort: ( - String sortId, - String? fieldId, - SortConditionPB? condition, - ) async { - final sort = state.sorts - .firstWhereOrNull((element) => element.sortId == sortId); - if (sort == null) { - return; - } - - final result = await _sortBackendSvc.updateSort( - sortId: sortId, - fieldId: fieldId ?? sort.fieldId, - condition: condition ?? sort.condition, - ); - result.fold((l) => {}, (err) => Log.error(err)); - }, - deleteAllSorts: () async { - final result = await _sortBackendSvc.deleteAllSorts(); - result.fold((l) => {}, (err) => Log.error(err)); - }, - didReceiveSorts: (sorts) { - emit(state.copyWith(sorts: sorts)); - }, - deleteSort: (sortId) async { - final result = await _sortBackendSvc.deleteSort( - sortId: sortId, - ); - result.fold((l) => null, (err) => Log.error(err)); - }, - reorderSort: (fromIndex, toIndex) async { - if (fromIndex < toIndex) { - toIndex--; - } - - final fromId = state.sorts[fromIndex].sortId; - final toId = state.sorts[toIndex].sortId; - - final newSorts = [...state.sorts]; - newSorts.insert(toIndex, newSorts.removeAt(fromIndex)); - emit(state.copyWith(sorts: newSorts)); - final result = await _sortBackendSvc.reorderSort( - fromSortId: fromId, - toSortId: toId, - ); - result.fold((l) => null, (err) => Log.error(err)); - }, - ); - }, - ); - } - - void _startListening() { - _onFieldFn = (fields) { - add(SortEditorEvent.didReceiveFields(List.from(fields))); - }; - _onSortsFn = (sorts) { - add(SortEditorEvent.didReceiveSorts(sorts)); - }; - - fieldController.addListener( - listenWhen: () => !isClosed, - onReceiveFields: _onFieldFn, - onSorts: _onSortsFn, - ); - } - - @override - Future close() async { - fieldController.removeListener( - onFieldsListener: _onFieldFn, - onSortsListener: _onSortsFn, - ); - _onFieldFn = null; - _onSortsFn = null; - return super.close(); - } -} - -@freezed -class SortEditorEvent with _$SortEditorEvent { - const factory SortEditorEvent.didReceiveFields(List fieldInfos) = - _DidReceiveFields; - const factory SortEditorEvent.didReceiveSorts(List sorts) = - _DidReceiveSorts; - const factory SortEditorEvent.createSort({ - required String fieldId, - SortConditionPB? condition, - }) = _CreateSort; - const factory SortEditorEvent.editSort({ - required String sortId, - String? fieldId, - SortConditionPB? condition, - }) = _EditSort; - const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) = - _ReorderSort; - const factory SortEditorEvent.deleteSort(String sortId) = _DeleteSort; - const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts; -} - -@freezed -class SortEditorState with _$SortEditorState { - const factory SortEditorState({ - required List sorts, - required List allFields, - required List creatableFields, - }) = _SortEditorState; - - factory SortEditorState.initial( - List sorts, - List fields, - ) { - return SortEditorState( - sorts: sorts, - allFields: fields, - creatableFields: _getCreatableSorts(fields), - ); - } -} - -List _getCreatableSorts(List fieldInfos) { - final List creatableFields = List.from(fieldInfos); - creatableFields.retainWhere( - (field) => field.fieldType.canCreateSort && !field.hasSort, - ); - return creatableFields; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/grid.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/grid.dart deleted file mode 100644 index b43c425a9d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/grid.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class GridPluginBuilder implements PluginBuilder { - @override - Plugin build(dynamic data) { - if (data is ViewPB) { - return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); - } else { - throw FlowyPluginException.invalidData; - } - } - - @override - String get menuName => LocaleKeys.grid_menuName.tr(); - - @override - FlowySvgData get icon => FlowySvgs.icon_grid_s; - - @override - PluginType get pluginType => PluginType.grid; - - @override - ViewLayoutPB get layoutType => ViewLayoutPB.Grid; -} - -class GridPluginConfig implements PluginConfig { - @override - bool get creatable => true; -} 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 deleted file mode 100755 index 4c9fd7bd61..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ /dev/null @@ -1,734 +0,0 @@ -import 'dart:async'; - -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: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: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'; -import 'widgets/footer/grid_footer.dart'; -import 'widgets/header/grid_header.dart'; -import 'widgets/row/row.dart'; -import 'widgets/shortcuts.dart'; - -class ToggleExtensionNotifier extends ChangeNotifier { - bool _isToggled = false; - - bool get isToggled => _isToggled; - - void toggle() { - _isToggled = !_isToggled; - notifyListeners(); - } -} - -class DesktopGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder { - final _toggleExtension = ToggleExtensionNotifier(); - - @override - Widget content( - BuildContext context, - ViewPB view, - DatabaseController controller, - bool shrinkWrap, - String? initialRowId, - ) { - return GridPage( - key: _makeValueKey(controller), - view: view, - databaseController: controller, - initialRowId: initialRowId, - shrinkWrap: shrinkWrap, - ); - } - - @override - Widget settingBar(BuildContext context, DatabaseController controller) { - return GridSettingBar( - key: _makeValueKey(controller), - controller: controller, - toggleExtension: _toggleExtension, - ); - } - - @override - Widget settingBarExtension( - BuildContext context, - DatabaseController controller, - ) { - return DatabaseViewSettingExtension( - key: _makeValueKey(controller), - viewId: controller.viewId, - databaseController: controller, - toggleExtension: _toggleExtension, - ); - } - - @override - void dispose() { - _toggleExtension.dispose(); - super.dispose(); - } - - ValueKey _makeValueKey(DatabaseController controller) { - return ValueKey(controller.viewId); - } -} - -class GridPage extends StatefulWidget { - const GridPage({ - super.key, - required this.view, - 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(); -} - -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()), - ), - ], - child: BlocListener( - listener: (context, state) { - final action = state.action; - if (action?.type == ActionType.openRow && - action?.objectId == widget.view.id) { - final rowId = action!.arguments?[ActionArgumentKeys.rowId]; - if (rowId != null) { - // If Reminder in existing database is pressed - // then open the row - _openRow(context, rowId); - } - } - }, - child: BlocConsumer( - listener: listener, - builder: (context, state) => state.loadingState.map( - idle: (_) => const SizedBox.shrink(), - 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)), - ), - ), - ), - ), - ); - } - - void _openRow(BuildContext context, String rowId) { - WidgetsBinding.instance.addPostFrameCallback((_) { - final gridBloc = context.read(); - final rowCache = gridBloc.rowCache; - final rowMeta = rowCache.getRow(rowId)?.rowMeta; - if (rowMeta == null) { - return; - } - - final rowController = RowController( - viewId: widget.view.id, - rowMeta: rowMeta, - rowCache: rowCache, - ); - - FlowyOverlay.show( - context: context, - builder: (_) => BlocProvider.value( - 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(); -} - -class _GridPageContentState extends State { - final _scrollController = GridScrollController( - scrollGroupController: LinkedScrollControllerGroup(), - ); - late final ScrollController headerScrollController; - - @override - void initState() { - super.initState(); - headerScrollController = _scrollController.linkHorizontalController(); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - @override - 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, - ), - ], - ); - } -} - -class _GridHeader extends StatelessWidget { - const _GridHeader({ - required this.headerScrollController, - required this.editable, - required this.shrinkWrap, - }); - - 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, - ), - ); - - if (!editable) { - child = IgnorePointer( - child: child, - ); - } - - return child; - } -} - -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(); - } - - 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; - }); - } - }); - } - - @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, - ), - ), - child: _shrinkWrapRenderList(context), - ), - ), - ); - } 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), - ), - ), - ); - } - - if (widget.shrinkWrap) { - return child; - } - - 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), - 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(), - }, - ), - description: LocaleKeys.grid_sort_removeSorting.tr(), - confirmLabel: LocaleKeys.button_remove.tr(), - closeOnAction: true, - onDelete: () { - SortBackendService(viewId: widget.viewId).deleteAllSorts(); - 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, - ); - }, - ); - } - - Widget _renderRow( - BuildContext context, - RowId rowId, { - required int index, - Animation? animation, - }) { - final databaseController = context.read().databaseController; - final DatabaseController(:viewId, :rowCache) = databaseController; - final rowMeta = rowCache.getRow(rowId)?.rowMeta; - - /// Return placeholder widget if the rowMeta is null. - if (rowMeta == null) { - Log.warn('RowMeta is null for rowId: $rowId'); - return const SizedBox.shrink(); - } - - final child = GridRow( - key: ValueKey("grid_row_$rowId"), - shrinkWrap: widget.shrinkWrap, - fieldController: databaseController.fieldController, - rowId: rowId, - viewId: viewId, - index: index, - editable: !context.watch().state.isLocked, - rowController: RowController( - viewId: viewId, - rowMeta: rowMeta, - rowCache: rowCache, - ), - 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(), - child: RowDetailPage( - rowController: RowController( - viewId: viewId, - rowMeta: rowMeta, - rowCache: rowCache, - ), - databaseController: databaseController, - userProfile: context.read().userProfile, - ), - ); - }, - ), - ); - - if (animation != null) { - return SizeTransition(sizeFactor: animation, child: child); - } - - return child; - } - - void moveRow(int from, int to) { - if (from != to) { - context.read().add(GridEvent.moveRow(from, to)); - } - } -} - -class _WrapScrollView extends StatelessWidget { - const _WrapScrollView({ - required this.contentWidth, - required this.scrollController, - required this.child, - }); - - final GridScrollController scrollController; - final double contentWidth; - final Widget child; - - @override - Widget build(BuildContext context) { - return ScrollbarListStack( - includeInsets: false, - axis: Axis.vertical, - controller: scrollController.verticalController, - barSize: GridSize.scrollBarSize, - autoHideScrollbar: false, - child: StyledSingleChildScrollView( - autoHideScrollbar: false, - includeInsets: false, - controller: scrollController.horizontalController, - axis: Axis.horizontal, - child: SizedBox( - width: contentWidth, - child: child, - ), - ), - ); - } -} - -/// This Widget is used to show the Calculations Row at the bottom of the Grids ScrollView -/// when the ScrollView is scrollable. -/// -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(); -} - -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, - ), - ), - ); - } -} 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 deleted file mode 100755 index c7402a17f9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart +++ /dev/null @@ -1,21 +0,0 @@ -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) { - if (fields.isEmpty) return 0; - - final fieldsWidth = fields - .where( - (element) => - element.visibility != null && - element.visibility != FieldVisibility.AlwaysHidden, - ) - .map((fieldInfo) => fieldInfo.width!.toDouble()) - .reduce((value, element) => value + element); - - return fieldsWidth + padding + GridSize.newPropertyButtonWidth; - } -} 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 deleted file mode 100755 index 78a8c97dae..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart +++ /dev/null @@ -1,53 +0,0 @@ -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 horizontalHeaderPadding => - UniversalPlatform.isDesktop ? 40 * scale : 16 * scale; - - static double get cellHPadding => 10 * scale; - - static double get cellVPadding => 8 * 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 typeOptionContentInsets => const EdgeInsets.all(4); - - static EdgeInsets get toolbarSettingButtonInsets => - const EdgeInsets.symmetric(horizontal: 6, vertical: 2); - - static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB( - GridSize.horizontalHeaderPadding, - 0, - UniversalPlatform.isMobile ? GridSize.horizontalHeaderPadding : 0, - UniversalPlatform.isMobile ? 100 : 0, - ); - - static EdgeInsets get contentInsets => EdgeInsets.symmetric( - horizontal: GridSize.horizontalHeaderPadding, - ); -} 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 deleted file mode 100644 index 17e4c0ed1d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart +++ /dev/null @@ -1,469 +0,0 @@ -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'; -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/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'; -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:linked_scroll_controller/linked_scroll_controller.dart'; - -import 'grid_scroll.dart'; -import 'layout/sizes.dart'; -import 'widgets/header/mobile_grid_header.dart'; -import 'widgets/mobile_fab.dart'; -import 'widgets/row/mobile_row.dart'; - -class MobileGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder { - @override - Widget content( - BuildContext context, - ViewPB view, - DatabaseController controller, - bool shrinkWrap, - String? initialRowId, - ) { - return MobileGridPage( - key: _makeValueKey(controller), - view: view, - databaseController: controller, - initialRowId: initialRowId, - shrinkWrap: shrinkWrap, - ); - } - - @override - Widget settingBar(BuildContext context, DatabaseController controller) => - const SizedBox.shrink(); - - @override - Widget settingBarExtension( - BuildContext context, - DatabaseController controller, - ) => - const SizedBox.shrink(); - - ValueKey _makeValueKey(DatabaseController controller) { - return ValueKey(controller.viewId); - } -} - -class MobileGridPage extends StatefulWidget { - const MobileGridPage({ - super.key, - required this.view, - 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(); -} - -class _MobileGridPageState extends State { - bool _didOpenInitialRow = false; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value( - value: getIt(), - ), - BlocProvider( - create: (context) => GridBloc( - view: widget.view, - databaseController: widget.databaseController, - )..add(const GridEvent.initial()), - ), - ], - child: BlocBuilder( - builder: (context, state) { - return state.loadingState.map( - loading: (_) => - const Center(child: CircularProgressIndicator.adaptive()), - finish: (result) { - _openRow(context, widget.initialRowId, true); - return result.successOrFail.fold( - (_) => GridPageContent( - view: widget.view, - shrinkWrap: widget.shrinkWrap, - ), - (err) => Center( - child: AppFlowyErrorPage( - error: err, - ), - ), - ); - }, - idle: (_) => const SizedBox.shrink(), - ); - }, - ), - ); - } - - void _openRow( - BuildContext context, - String? rowId, [ - bool initialRow = false, - ]) { - if (rowId != null && (!initialRow || (initialRow && !_didOpenInitialRow))) { - _didOpenInitialRow = initialRow; - - WidgetsBinding.instance.addPostFrameCallback((_) { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: rowId, - MobileRowDetailPage.argDatabaseController: - widget.databaseController, - }, - ); - }); - } - } -} - -class GridPageContent extends StatefulWidget { - const GridPageContent({ - super.key, - required this.view, - this.shrinkWrap = false, - }); - - final ViewPB view; - final bool shrinkWrap; - - @override - State createState() => _GridPageContentState(); -} - -class _GridPageContentState extends State { - final _scrollController = GridScrollController( - scrollGroupController: LinkedScrollControllerGroup(), - ); - late final ScrollController contentScrollController; - late final ScrollController reorderableController; - - @override - void initState() { - super.initState(); - contentScrollController = _scrollController.linkHorizontalController(); - reorderableController = _scrollController.linkHorizontalController(); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isLocked = - context.read()?.state.isLocked ?? false; - return BlocListener( - listenWhen: (previous, current) => - previous.createdRow != current.createdRow, - listener: (context, state) { - if (state.createdRow == null || !state.openRowDetail) { - return; - } - final bloc = context.read(); - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: state.createdRow!.id, - MobileRowDetailPage.argDatabaseController: bloc.databaseController, - }, - ); - bloc.add(const GridEvent.resetCreatedRow()); - }, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _GridHeader( - contentScrollController: contentScrollController, - reorderableController: reorderableController, - ), - _GridRows( - viewId: widget.view.id, - scrollController: _scrollController, - ), - ], - ), - if (!widget.shrinkWrap && !isLocked) - Positioned( - bottom: 16, - right: 16, - child: getGridFabs(context), - ), - ], - ), - ); - } -} - -class _GridHeader extends StatelessWidget { - const _GridHeader({ - required this.contentScrollController, - required this.reorderableController, - }); - - final ScrollController contentScrollController; - final ScrollController reorderableController; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return MobileGridHeader( - viewId: state.viewId, - contentScrollController: contentScrollController, - reorderableController: reorderableController, - ); - }, - ); - } -} - -class _GridRows extends StatelessWidget { - const _GridRows({ - required this.viewId, - required this.scrollController, - }); - - final String viewId; - final GridScrollController scrollController; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.fields != current.fields, - builder: (context, state) { - final double contentWidth = getMobileGridContentWidth(state.fields); - return Flexible( - child: _WrapScrollView( - scrollController: scrollController, - contentWidth: contentWidth, - child: BlocBuilder( - buildWhen: (previous, current) => current.reason.maybeWhen( - reorderRows: () => true, - reorderSingleRow: (reorderRow, rowInfo) => true, - delete: (item) => true, - insert: (item) => true, - orElse: () => false, - ), - builder: (context, state) { - final behavior = ScrollConfiguration.of(context).copyWith( - scrollbars: false, - physics: const ClampingScrollPhysics(), - ); - return ScrollConfiguration( - behavior: behavior, - child: _renderList(context, state), - ); - }, - ), - ), - ); - }, - ); - } - - Widget _renderList( - BuildContext context, - GridState state, - ) { - final children = state.rowInfos.mapIndexed((index, rowInfo) { - return ReorderableDelayedDragStartListener( - key: ValueKey(rowInfo.rowMeta.id), - index: index, - child: _renderRow( - context, - rowInfo.rowId, - isDraggable: state.reorderable, - index: index, - ), - ); - }).toList(); - - return ReorderableListView.builder( - scrollController: scrollController.verticalController, - buildDefaultDragHandles: false, - shrinkWrap: true, - proxyDecorator: (child, index, animation) => Material( - color: Colors.transparent, - child: child, - ), - onReorder: (fromIndex, newIndex) { - final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; - if (fromIndex == toIndex) { - return; - } - context.read().add(GridEvent.moveRow(fromIndex, toIndex)); - }, - itemCount: state.rowInfos.length, - itemBuilder: (context, index) => children[index], - footer: Padding( - padding: GridSize.footerContentInsets, - child: _AddRowButton(), - ), - ); - } - - Widget _renderRow( - BuildContext context, - RowId rowId, { - int? index, - required bool isDraggable, - Animation? animation, - }) { - final rowMeta = context - .read() - .databaseController - .rowCache - .getRow(rowId) - ?.rowMeta; - - if (rowMeta == null) { - Log.warn('RowMeta is null for rowId: $rowId'); - return const SizedBox.shrink(); - } - - final databaseController = context.read().databaseController; - - Widget child = MobileGridRow( - key: ValueKey(rowMeta.id), - rowId: rowId, - isDraggable: isDraggable, - databaseController: databaseController, - openDetailPage: (context) { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: rowId, - MobileRowDetailPage.argDatabaseController: databaseController, - }, - ); - }, - ); - - if (animation != null) { - child = SizeTransition( - sizeFactor: animation, - child: child, - ); - } - - final isLocked = - context.read()?.state.isLocked ?? false; - if (isLocked) { - child = IgnorePointer( - child: child, - ); - } - - return child; - } -} - -class _WrapScrollView extends StatelessWidget { - const _WrapScrollView({ - required this.contentWidth, - required this.scrollController, - required this.child, - }); - - final GridScrollController scrollController; - final double contentWidth; - final Widget child; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - controller: scrollController.horizontalController, - scrollDirection: Axis.horizontal, - child: SizedBox( - width: contentWidth, - child: child, - ), - ); - } -} - -class _AddRowButton extends StatelessWidget { - @override - Widget build(BuildContext context) { - final borderSide = BorderSide( - color: Theme.of(context).dividerColor, - ); - const radius = BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), - ); - final decoration = BoxDecoration( - borderRadius: radius, - border: BorderDirectional( - start: borderSide, - end: borderSide, - bottom: borderSide, - ), - ); - return Container( - height: 54, - decoration: decoration, - child: FlowyButton( - text: FlowyText( - LocaleKeys.grid_row_newRow.tr(), - fontSize: 15, - color: Theme.of(context).hintColor, - ), - margin: const EdgeInsets.symmetric(horizontal: 20.0), - radius: radius, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: () => context.read().add(const GridEvent.createRow()), - leftIcon: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).hintColor, - size: const Size.square(18), - ), - leftIconSize: const Size.square(18), - ), - ); - } -} - -double getMobileGridContentWidth(List fields) { - final visibleFields = fields.where( - (field) => field.visibility != FieldVisibility.AlwaysHidden, - ); - return (visibleFields.length + 1) * 200 + - GridSize.horizontalHeaderPadding * 2; -} 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 deleted file mode 100644 index 5ea364bc72..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart +++ /dev/null @@ -1,210 +0,0 @@ -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'; -import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart'; -import 'package:appflowy/plugins/database/grid/application/calculations/field_type_calc_ext.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart'; -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: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 CalculateCell extends StatefulWidget { - const CalculateCell({ - super.key, - required this.fieldInfo, - required this.width, - this.calculation, - }); - - final FieldInfo fieldInfo; - final double width; - final CalculationPB? calculation; - - @override - State createState() => _CalculateCellState(); -} - -class _CalculateCellState extends State { - final _cellScrollController = ScrollController(); - bool isSelected = false; - bool isScrollable = false; - - @override - void initState() { - super.initState(); - _checkScrollable(); - } - - @override - void didUpdateWidget(covariant CalculateCell oldWidget) { - _checkScrollable(); - super.didUpdateWidget(oldWidget); - } - - void _checkScrollable() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_cellScrollController.hasClients) { - setState( - () => - isScrollable = _cellScrollController.position.maxScrollExtent > 0, - ); - } - }); - } - - @override - void dispose() { - _cellScrollController.dispose(); - super.dispose(); - } - - void setIsSelected(bool selected) => setState(() => isSelected = selected); - - @override - Widget build(BuildContext context) { - final prefix = _prefixFromFieldType(widget.fieldInfo.fieldType); - - return SizedBox( - height: 35, - width: widget.width, - child: AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(150, 200)), - direction: PopoverDirection.bottomWithCenterAligned, - onClose: () => setIsSelected(false), - popupBuilder: (_) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setIsSelected(true); - } - }); - - return SingleChildScrollView( - child: Column( - children: [ - if (widget.calculation != null) - RemoveCalculationButton( - onTap: () => context.read().add( - CalculationsEvent.removeCalculation( - widget.fieldInfo.id, - widget.calculation!.id, - ), - ), - ), - ...widget.fieldInfo.fieldType.calculationsForFieldType().map( - (type) => CalculationTypeItem( - type: type, - onTap: () { - if (type != widget.calculation?.calculationType) { - context.read().add( - CalculationsEvent.updateCalculationType( - widget.fieldInfo.id, - type, - calculationId: widget.calculation?.id, - ), - ); - } - }, - ), - ), - ], - ), - ); - }, - child: widget.calculation != null - ? _showCalculateValue(context, prefix) - : CalculationSelector(isSelected: isSelected), - ), - ); - } - - Widget _showCalculateValue(BuildContext context, String? prefix) { - prefix = prefix != null ? '$prefix ' : ''; - final calculateValue = - '$prefix${_withoutTrailingZeros(widget.calculation!.value)}'; - - return FlowyTooltip( - message: !isScrollable ? "" : null, - richMessage: !isScrollable - ? null - : TextSpan( - children: [ - TextSpan( - text: widget.calculation!.calculationType.shortLabel - .toUpperCase(), - style: context.tooltipTextStyle(), - ), - const TextSpan(text: ' '), - TextSpan( - text: calculateValue, - style: context - .tooltipTextStyle() - ?.copyWith(fontWeight: FontWeight.w500), - ), - ], - ), - child: FlowyButton( - radius: BorderRadius.zero, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: Row( - children: [ - Expanded( - child: SingleChildScrollView( - controller: _cellScrollController, - key: ValueKey(widget.calculation!.id), - reverse: true, - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FlowyText( - lineHeight: 1.0, - widget.calculation!.calculationType.shortLabel - .toUpperCase(), - color: Theme.of(context).hintColor, - fontSize: 10, - ), - if (widget.calculation!.value.isNotEmpty) ...[ - const HSpace(8), - FlowyText( - lineHeight: 1.0, - calculateValue, - color: AFThemeExtension.of(context).textColor, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ), - ), - ], - ), - ), - ); - } - - String _withoutTrailingZeros(String value) { - if (trailingZerosRegex.hasMatch(value)) { - final match = trailingZerosRegex.firstMatch(value)!; - return match.group(1)!; - } - - return value; - } - - String? _prefixFromFieldType(FieldType fieldType) => switch (fieldType) { - FieldType.Number => - NumberTypeOptionPB.fromBuffer(widget.fieldInfo.field.typeOptionData) - .format - .iconSymbol(false), - _ => null, - }; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart deleted file mode 100644 index b95295818c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart +++ /dev/null @@ -1,61 +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/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; - -class CalculationSelector extends StatefulWidget { - const CalculationSelector({ - super.key, - required this.isSelected, - }); - - final bool isSelected; - - @override - State createState() => _CalculationSelectorState(); -} - -class _CalculationSelectorState extends State { - bool _isHovering = false; - - void _setHovering(bool isHovering) => - setState(() => _isHovering = isHovering); - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => _setHovering(true), - onExit: (_) => _setHovering(false), - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: widget.isSelected || _isHovering ? 1 : 0, - child: FlowyButton( - radius: BorderRadius.zero, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - child: FlowyText( - LocaleKeys.grid_calculate.tr(), - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(8), - FlowySvg( - FlowySvgs.arrow_down_s, - color: Theme.of(context).hintColor, - ), - ], - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index b1b696d790..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pbenum.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; - -class CalculationTypeItem extends StatelessWidget { - const CalculationTypeItem({ - super.key, - required this.type, - required this.onTap, - }); - - final CalculationType type; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - type.label, - overflow: TextOverflow.ellipsis, - lineHeight: 1.0, - ), - 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 deleted file mode 100644 index 5524633a46..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart +++ /dev/null @@ -1,54 +0,0 @@ -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/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 { - const GridCalculationsRow({ - super.key, - required this.viewId, - this.includeDefaultInsets = true, - }); - - final String viewId; - final bool includeDefaultInsets; - - @override - Widget build(BuildContext context) { - final gridBloc = context.read(); - - return BlocProvider( - create: (context) => CalculationsBloc( - viewId: gridBloc.databaseController.viewId, - fieldController: gridBloc.databaseController.fieldController, - )..add(const CalculationsEvent.started()), - child: BlocBuilder( - builder: (context, state) { - final padding = - context.read().horizontalPadding; - return Padding( - padding: includeDefaultInsets - ? EdgeInsets.symmetric(horizontal: padding) - : EdgeInsets.zero, - child: Row( - children: [ - ...state.fields.map( - (field) => CalculateCell( - key: Key( - '${field.id}-${state.calculationsByFieldId[field.id]?.id}', - ), - width: field.width!.toDouble(), - fieldInfo: field, - calculation: state.calculationsByFieldId[field.id], - ), - ), - ], - ), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 982ee992b6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.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'; - -class RemoveCalculationButton extends StatelessWidget { - const RemoveCalculationButton({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - LocaleKeys.grid_calculationTypeLabel_none.tr(), - overflow: TextOverflow.ellipsis, - ), - onTap: () { - onTap(); - PopoverContainer.of(context).close(); - }, - ), - ); - } -} 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 deleted file mode 100644 index bda8634cdb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.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/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/checkbox_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 CheckboxFilterChoicechip extends StatelessWidget { - const CheckboxFilterChoicechip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @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, - ); - }, - ), - ); - } -} - -class CheckboxFilterEditor extends StatefulWidget { - const CheckboxFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; - - @override - State createState() => _CheckboxFilterEditorState(); -} - -class _CheckboxFilterEditorState 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 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; - } - }, - ), - ], - ), - ), - ); - }, - ); - } -} - -class CheckboxFilterConditionList extends StatelessWidget { - const CheckboxFilterConditionList({ - super.key, - required this.filter, - required this.popoverMutex, - required this.onCondition, - }); - - final CheckboxFilter filter; - final PopoverMutex popoverMutex; - final void Function(CheckboxFilterConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - return PopoverActionList( - asBarrier: true, - mutex: popoverMutex, - direction: PopoverDirection.bottomWithCenterAligned, - actions: CheckboxFilterConditionPB.values - .map( - (action) => ConditionWrapper( - action, - filter.condition == action, - ), - ) - .toList(), - buildChild: (controller) { - return ConditionButton( - conditionName: filter.conditionName, - onTap: () => controller.show(), - ); - }, - onSelected: (action, controller) { - onCondition(action.inner); - controller.close(); - }, - ); - } -} - -class ConditionWrapper extends ActionCell { - ConditionWrapper(this.inner, this.isSelected); - - final CheckboxFilterConditionPB inner; - final bool isSelected; - - @override - Widget? rightIcon(Color iconColor) => - isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - - @override - String get name => inner.filterName; -} - -extension TextFilterConditionPBExtension on CheckboxFilterConditionPB { - String get filterName { - switch (this) { - case CheckboxFilterConditionPB.IsChecked: - return LocaleKeys.grid_checkboxFilter_isChecked.tr(); - case CheckboxFilterConditionPB.IsUnChecked: - return LocaleKeys.grid_checkboxFilter_isUnchecked.tr(); - default: - return ""; - } - } -} 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/choicechip.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart deleted file mode 100644 index 99804ad69b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart +++ /dev/null @@ -1,113 +0,0 @@ -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: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 ChoiceChipButton extends StatelessWidget { - const ChoiceChipButton({ - super.key, - required this.fieldInfo, - this.filterDesc = '', - this.onTap, - }); - - final FieldInfo fieldInfo; - final String filterDesc; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - final buttonText = - filterDesc.isEmpty ? fieldInfo.name : "${fieldInfo.name}: $filterDesc"; - - 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)), - ), - useIntrinsicWidth: true, - text: FlowyText( - buttonText, - lineHeight: 1.0, - color: AFThemeExtension.of(context).textColor, - ), - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - radius: const BorderRadius.all(Radius.circular(14)), - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - ), - rightIcon: const _ChoicechipDownArrow(), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: onTap, - ), - ); - } -} - -class _ChoicechipDownArrow extends StatelessWidget { - const _ChoicechipDownArrow(); - - @override - Widget build(BuildContext context) { - return 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); - }, - ); - } -} 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 deleted file mode 100644 index a2d61cc5f4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart +++ /dev/null @@ -1,420 +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/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 'choicechip.dart'; - -class DateFilterChoicechip extends StatelessWidget { - const DateFilterChoicechip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @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; - } -} 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 deleted file mode 100644 index cc38b4eaba..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.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/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/workspace/presentation/widgets/pop_up_action.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../condition_button.dart'; -import '../disclosure_button.dart'; - -import 'choicechip.dart'; - -class NumberFilterChoiceChip extends StatelessWidget { - const NumberFilterChoiceChip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @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), - ); - }, - ), - ); - } -} - -class NumberFilterEditor extends StatefulWidget { - const NumberFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; - - @override - State createState() => _NumberFilterEditorState(); -} - -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) { - final List children = [ - _buildFilterPanel(filter, field), - if (filter.condition != NumberFilterConditionPB.NumberIsEmpty && - filter.condition != NumberFilterConditionPB.NumberIsNotEmpty) ...[ - const VSpace(4), - _buildFilterNumberField(filter), - ], - ]; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: IntrinsicHeight(child: Column(children: children)), - ); - }, - ); - } - - Widget _buildFilterPanel( - NumberFilter filter, - FieldInfo field, - ) { - return SizedBox( - height: 20, - child: Row( - children: [ - Expanded( - child: FlowyText( - field.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(4), - Expanded( - child: NumberFilterConditionList( - filter: filter, - popoverMutex: popoverMutex, - onCondition: (condition) { - final newFilter = filter.copyWith(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 _buildFilterNumberField( - NumberFilter filter, - ) { - return FlowyTextField( - text: 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)); - }, - ); - } -} - -class NumberFilterConditionList extends StatelessWidget { - const NumberFilterConditionList({ - super.key, - required this.filter, - required this.popoverMutex, - required this.onCondition, - }); - - final NumberFilter filter; - final PopoverMutex popoverMutex; - final void Function(NumberFilterConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - return PopoverActionList( - asBarrier: true, - mutex: popoverMutex, - direction: PopoverDirection.bottomWithCenterAligned, - actions: NumberFilterConditionPB.values - .map( - (action) => ConditionWrapper( - action, - filter.condition == 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, this.isSelected); - - final NumberFilterConditionPB inner; - final bool isSelected; - - @override - Widget? rightIcon(Color iconColor) => - isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - - @override - String get name => inner.filterName; -} - -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(), - NumberFilterConditionPB.NotEqual => - LocaleKeys.grid_numberFilter_notEqual.tr(), - NumberFilterConditionPB.LessThan => - LocaleKeys.grid_numberFilter_lessThan.tr(), - NumberFilterConditionPB.LessThanOrEqualTo => - LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(), - NumberFilterConditionPB.GreaterThan => - LocaleKeys.grid_numberFilter_greaterThan.tr(), - NumberFilterConditionPB.GreaterThanOrEqualTo => - LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(), - NumberFilterConditionPB.NumberIsEmpty => - LocaleKeys.grid_numberFilter_isEmpty.tr(), - NumberFilterConditionPB.NumberIsNotEmpty => - LocaleKeys.grid_numberFilter_isNotEmpty.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 deleted file mode 100644 index a8c8f69016..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart +++ /dev/null @@ -1,91 +0,0 @@ -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/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:flutter/widgets.dart'; - -class SelectOptionFilterConditionList extends StatelessWidget { - const SelectOptionFilterConditionList({ - super.key, - required this.filter, - required this.fieldType, - required this.popoverMutex, - required this.onCondition, - }); - - final SelectOptionFilter filter; - final FieldType fieldType; - final PopoverMutex popoverMutex; - final void Function(SelectOptionFilterConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - final conditions = (fieldType == FieldType.SingleSelect - ? SingleSelectOptionFilterCondition().conditions - : MultiSelectOptionFilterCondition().conditions); - return PopoverActionList( - asBarrier: true, - mutex: popoverMutex, - direction: PopoverDirection.bottomWithCenterAligned, - actions: conditions - .map( - (action) => ConditionWrapper( - action.$1, - filter.condition == action.$1, - ), - ) - .toList(), - buildChild: (controller) { - return ConditionButton( - conditionName: filter.condition.i18n, - onTap: () => controller.show(), - ); - }, - onSelected: (action, controller) async { - onCondition(action.inner); - controller.close(); - }, - ); - } -} - -class ConditionWrapper extends ActionCell { - ConditionWrapper(this.inner, this.isSelected); - - final SelectOptionFilterConditionPB inner; - final bool isSelected; - - @override - Widget? rightIcon(Color iconColor) { - return isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - } - - @override - String get name => inner.i18n; -} - -extension SelectOptionFilterConditionPBExtension - on SelectOptionFilterConditionPB { - String get i18n { - return switch (this) { - SelectOptionFilterConditionPB.OptionIs => - LocaleKeys.grid_selectOptionFilter_is.tr(), - SelectOptionFilterConditionPB.OptionIsNot => - LocaleKeys.grid_selectOptionFilter_isNot.tr(), - SelectOptionFilterConditionPB.OptionContains => - LocaleKeys.grid_selectOptionFilter_contains.tr(), - SelectOptionFilterConditionPB.OptionDoesNotContain => - LocaleKeys.grid_selectOptionFilter_doesNotContain.tr(), - SelectOptionFilterConditionPB.OptionIsEmpty => - LocaleKeys.grid_selectOptionFilter_isEmpty.tr(), - SelectOptionFilterConditionPB.OptionIsNotEmpty => - LocaleKeys.grid_selectOptionFilter_isNotEmpty.tr(), - _ => "", - }; - } -} 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 deleted file mode 100644 index 3e9db2df1b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart +++ /dev/null @@ -1,112 +0,0 @@ -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/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: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'; - -class SelectOptionFilterList extends StatelessWidget { - const SelectOptionFilterList({ - super.key, - required this.filter, - required this.field, - required this.options, - required this.onTap, - }); - - final SelectOptionFilter filter; - final FieldInfo field; - final List options; - final VoidCallback onTap; - - @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), - ); - }, - ); - } - - 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), - ), - ], - ), - ), - ); - } -} 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 deleted file mode 100644 index 50984bbf3d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart +++ /dev/null @@ -1,146 +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/grid/application/filter/filter_editor_bloc.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 '../choicechip.dart'; - -import 'condition_list.dart'; -import 'option_list.dart'; - -class SelectOptionFilterChoicechip extends StatelessWidget { - const SelectOptionFilterChoicechip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @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), - ); - }, - ), - ); - } -} - -class SelectOptionFilterEditor extends StatefulWidget { - const SelectOptionFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; - - @override - State createState() => - _SelectOptionFilterEditorState(); -} - -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)), - ]; - - if (filter.canAttachContent) { - slivers - ..add(const SliverToBoxAdapter(child: VSpace(4))) - ..add( - SliverToBoxAdapter( - child: SelectOptionFilterList( - filter: filter, - field: field, - options: filter.makeDelegate(field).getOptions(field), - onTap: () => popoverMutex.close(), - ), - ), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: CustomScrollView( - shrinkWrap: true, - slivers: slivers, - physics: StyledScrollPhysics(), - ), - ); - }, - ); - } - - Widget _buildFilterPanel( - SelectOptionFilter filter, - FieldInfo field, - ) { - return SizedBox( - height: 20, - child: Row( - children: [ - Expanded( - child: FlowyText( - field.field.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(4), - SelectOptionFilterConditionList( - filter: filter, - fieldType: field.fieldType, - 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; - } - }, - ), - ], - ), - ); - } -} 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 deleted file mode 100644 index 548f21efbe..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart +++ /dev/null @@ -1,253 +0,0 @@ -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/workspace/presentation/widgets/pop_up_action.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../condition_button.dart'; -import '../disclosure_button.dart'; - -import 'choicechip.dart'; - -class TextFilterChoicechip extends StatelessWidget { - const TextFilterChoicechip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @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), - ); - }, - ), - ); - } -} - -class TextFilterEditor extends StatefulWidget { - const TextFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; - - @override - State createState() => _TextFilterEditorState(); -} - -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) { - final List children = [ - _buildFilterPanel(filter, field), - ]; - - if (filter.condition != TextFilterConditionPB.TextIsEmpty && - filter.condition != TextFilterConditionPB.TextIsNotEmpty) { - children.add(const VSpace(4)); - children.add(_buildFilterTextField(filter, field)); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: IntrinsicHeight(child: Column(children: children)), - ); - }, - ); - } - - Widget _buildFilterPanel(TextFilter filter, FieldInfo field) { - return SizedBox( - height: 20, - child: Row( - children: [ - Expanded( - child: FlowyText( - field.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(4), - Expanded( - child: TextFilterConditionList( - filter: filter, - popoverMutex: popoverMutex, - onCondition: (condition) { - final newFilter = filter.copyWith(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 _buildFilterTextField(TextFilter filter, FieldInfo field) { - return FlowyTextField( - text: 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)); - }, - ); - } -} - -class TextFilterConditionList extends StatelessWidget { - const TextFilterConditionList({ - super.key, - required this.filter, - required this.popoverMutex, - required this.onCondition, - }); - - final TextFilter filter; - final PopoverMutex popoverMutex; - final void Function(TextFilterConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - return PopoverActionList( - asBarrier: true, - mutex: popoverMutex, - direction: PopoverDirection.bottomWithCenterAligned, - actions: TextFilterConditionPB.values - .map( - (action) => ConditionWrapper( - action, - filter.condition == action, - ), - ) - .toList(), - buildChild: (controller) { - return ConditionButton( - conditionName: filter.condition.filterName, - onTap: () => controller.show(), - ); - }, - onSelected: (action, controller) async { - onCondition(action.inner); - controller.close(); - }, - ); - } -} - -class ConditionWrapper extends ActionCell { - ConditionWrapper(this.inner, this.isSelected); - - final TextFilterConditionPB inner; - final bool isSelected; - - @override - Widget? rightIcon(Color iconColor) { - if (isSelected) { - return const FlowySvg(FlowySvgs.check_s); - } else { - return null; - } - } - - @override - String get name => inner.filterName; -} - -extension TextFilterConditionPBExtension on TextFilterConditionPB { - String get filterName { - switch (this) { - case TextFilterConditionPB.TextContains: - return LocaleKeys.grid_textFilter_contains.tr(); - case TextFilterConditionPB.TextDoesNotContain: - return LocaleKeys.grid_textFilter_doesNotContain.tr(); - case TextFilterConditionPB.TextEndsWith: - return LocaleKeys.grid_textFilter_endsWith.tr(); - case TextFilterConditionPB.TextIs: - return LocaleKeys.grid_textFilter_is.tr(); - case TextFilterConditionPB.TextIsNot: - return LocaleKeys.grid_textFilter_isNot.tr(); - case TextFilterConditionPB.TextStartsWith: - return LocaleKeys.grid_textFilter_startWith.tr(); - case TextFilterConditionPB.TextIsEmpty: - return LocaleKeys.grid_textFilter_isEmpty.tr(); - case TextFilterConditionPB.TextIsNotEmpty: - return LocaleKeys.grid_textFilter_isNotEmpty.tr(); - default: - return ""; - } - } - - String get choicechipPrefix { - switch (this) { - case TextFilterConditionPB.TextDoesNotContain: - return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr(); - case TextFilterConditionPB.TextEndsWith: - return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr(); - case TextFilterConditionPB.TextIsNot: - return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr(); - case TextFilterConditionPB.TextStartsWith: - return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr(); - case TextFilterConditionPB.TextIsEmpty: - return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr(); - case TextFilterConditionPB.TextIsNotEmpty: - return LocaleKeys.grid_textFilter_choicechipPrefix_isNotEmpty.tr(); - default: - return ""; - } - } -} 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 deleted file mode 100644 index dcd33f66c3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart +++ /dev/null @@ -1,230 +0,0 @@ -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/workspace/presentation/widgets/pop_up_action.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../condition_button.dart'; -import '../disclosure_button.dart'; - -import 'choicechip.dart'; - -class TimeFilterChoiceChip extends StatelessWidget { - const TimeFilterChoiceChip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @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, - ); - }, - ), - ); - } -} - -class TimeFilterEditor extends StatefulWidget { - const TimeFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; - @override - State createState() => _TimeFilterEditorState(); -} - -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) { - final List children = [ - _buildFilterPanel(filter, field), - if (filter.condition != NumberFilterConditionPB.NumberIsEmpty && - filter.condition != NumberFilterConditionPB.NumberIsNotEmpty) ...[ - const VSpace(4), - _buildFilterTimeField(filter, field), - ], - ]; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: IntrinsicHeight(child: Column(children: children)), - ); - }, - ); - } - - Widget _buildFilterPanel( - TimeFilter filter, - FieldInfo field, - ) { - return SizedBox( - height: 20, - child: Row( - children: [ - Expanded( - child: FlowyText( - field.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(4), - Expanded( - child: TimeFilterConditionList( - filter: filter, - popoverMutex: popoverMutex, - onCondition: (condition) { - final newFilter = filter.copyWith(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 _buildFilterTimeField( - TimeFilter filter, - FieldInfo field, - ) { - return FlowyTextField( - text: 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)); - }, - ); - } -} - -class TimeFilterConditionList extends StatelessWidget { - const TimeFilterConditionList({ - super.key, - required this.filter, - required this.popoverMutex, - required this.onCondition, - }); - - final TimeFilter filter; - final PopoverMutex popoverMutex; - final void Function(NumberFilterConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - return PopoverActionList( - asBarrier: true, - mutex: popoverMutex, - direction: PopoverDirection.bottomWithCenterAligned, - actions: NumberFilterConditionPB.values - .map( - (action) => ConditionWrapper( - action, - filter.condition == 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, this.isSelected); - - final NumberFilterConditionPB inner; - final bool isSelected; - - @override - Widget? rightIcon(Color iconColor) => - isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - - @override - String get name => inner.filterName; -} - -extension TimeFilterConditionPBExtension on NumberFilterConditionPB { - String get filterName { - return switch (this) { - NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(), - NumberFilterConditionPB.NotEqual => - LocaleKeys.grid_numberFilter_notEqual.tr(), - NumberFilterConditionPB.LessThan => - LocaleKeys.grid_numberFilter_lessThan.tr(), - NumberFilterConditionPB.LessThanOrEqualTo => - LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(), - NumberFilterConditionPB.GreaterThan => - LocaleKeys.grid_numberFilter_greaterThan.tr(), - NumberFilterConditionPB.GreaterThanOrEqualTo => - LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(), - NumberFilterConditionPB.NumberIsEmpty => - LocaleKeys.grid_numberFilter_isEmpty.tr(), - NumberFilterConditionPB.NumberIsNotEmpty => - LocaleKeys.grid_numberFilter_isNotEmpty.tr(), - _ => "", - }; - } -} 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 deleted file mode 100644 index 7e453b9ab7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart +++ /dev/null @@ -1,40 +0,0 @@ -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/filter/choicechip/text.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'choicechip.dart'; - -class URLFilterChoicechip extends StatelessWidget { - const URLFilterChoicechip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @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), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 9cf2cd8322..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart +++ /dev/null @@ -1,148 +0,0 @@ -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: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/scrolling/styled_list.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class CreateDatabaseViewFilterList extends StatelessWidget { - const CreateDatabaseViewFilterList({ - super.key, - this.onTap, - }); - - final VoidCallback? onTap; - - @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, - listener: (context, state) { - context - .read>() - .add(SimpleTextFilterEvent.receiveNewValues(state.fields)); - }, - child: BlocBuilder, - SimpleTextFilterState>( - builder: (context, state) { - final cells = state.values.map((fieldInfo) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FilterableFieldButton( - fieldInfo: fieldInfo, - onTap: () { - context - .read() - .add(FilterEditorEvent.createFilter(fieldInfo)); - onTap?.call(); - }, - ), - ); - }).toList(); - - final List slivers = [ - SliverPersistentHeader( - pinned: true, - delegate: _FilterTextFieldDelegate(), - ), - SliverToBoxAdapter( - child: ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - itemBuilder: (_, int index) => cells[index], - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - ), - ), - ]; - return CustomScrollView( - shrinkWrap: true, - slivers: slivers, - physics: StyledScrollPhysics(), - ); - }, - ), - ), - ); - } -} - -class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate { - _FilterTextFieldDelegate(); - - double fixHeight = 36; - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return Container( - padding: const EdgeInsets.only(bottom: 4), - color: Theme.of(context).cardColor, - height: fixHeight, - child: FlowyTextField( - hintText: LocaleKeys.grid_settings_filterBy.tr(), - onChanged: (text) { - context - .read>() - .add(SimpleTextFilterEvent.updateFilter(text)); - }, - ), - ); - } - - @override - double get maxExtent => fixHeight; - - @override - double get minExtent => fixHeight; - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { - return false; - } -} - -class FilterableFieldButton extends StatelessWidget { - const FilterableFieldButton({ - super.key, - required this.fieldInfo, - required this.onTap, - }); - - final FieldInfo fieldInfo; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, - fieldInfo.field.name, - color: AFThemeExtension.of(context).textColor, - ), - onTap: onTap, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - ), - ); - } -} 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 deleted file mode 100644 index c91b47e2b7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart +++ /dev/null @@ -1,124 +0,0 @@ -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: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'; - -import 'create_filter_list.dart'; -import 'filter_menu_item.dart'; - -class FilterMenu extends StatelessWidget { - const FilterMenu({ - super.key, - required this.fieldController, - }); - - final FieldController fieldController; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FilterEditorBloc( - 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); - }, - 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, - ), - ) - .toList(), - ); - - if (state.fields.isNotEmpty) { - children.add( - AddFilterButton( - viewId: state.viewId, - ), - ); - } - - return Wrap( - spacing: 6, - runSpacing: 4, - children: children, - ); - }, - ), - ); - } -} - -class AddFilterButton extends StatefulWidget { - const AddFilterButton({required this.viewId, super.key}); - - final String viewId; - - @override - State createState() => _AddFilterButtonState(); -} - -class _AddFilterButtonState extends State { - final PopoverController popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return wrapPopover( - SizedBox( - height: 28, - child: FlowyButton( - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.grid_settings_addFilter.tr(), - color: AFThemeExtension.of(context).textColor, - ), - useIntrinsicWidth: true, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - leftIcon: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).iconTheme.color, - ), - onTap: () => popoverController.show(), - ), - ), - ); - } - - Widget wrapPopover(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(), - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index d7e45840e6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/material.dart'; - -import 'choicechip/checkbox.dart'; -import 'choicechip/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'; - -class FilterMenuItem extends StatelessWidget { - const FilterMenuItem({ - super.key, - required this.fieldType, - required this.filterId, - }); - - final FieldType fieldType; - final String filterId; - - @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 || - FieldType.MultiSelect => - SelectOptionFilterChoicechip(filterId: filterId), - // FieldType.Time => - // TimeFilterChoiceChip(filterInfo: filterInfo), - _ => const SizedBox.shrink(), - }; - } -} 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 deleted file mode 100755 index 43a0301a10..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.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/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 { - const GridAddRowButton({super.key}); - - @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), - ), - ), - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.grid_row_newRow.tr(), - color: color, - ), - 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, - ), - ); - } -} - -class GridRowBottomBar extends StatelessWidget { - const GridRowBottomBar({super.key}); - - @override - Widget build(BuildContext context) { - final padding = - context.read().horizontalPadding; - return Container( - padding: GridSize.footerContentInsets.copyWith(left: 0) + - EdgeInsets.only(left: padding), - 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 deleted file mode 100755 index 915bf70a61..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ /dev/null @@ -1,280 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart'; -import 'package:appflowy/plugins/database/application/field/field_controller.dart'; -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:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../layout/sizes.dart'; - -class GridFieldCell extends StatefulWidget { - const GridFieldCell({ - super.key, - required this.viewId, - required this.fieldController, - required this.fieldInfo, - required this.onTap, - required this.onEditorOpened, - required this.onFieldInsertedOnEitherSide, - required this.isEditing, - required this.isNew, - }); - - final String viewId; - final FieldController fieldController; - final FieldInfo fieldInfo; - final VoidCallback onTap; - final VoidCallback onEditorOpened; - final void Function(String fieldId) onFieldInsertedOnEitherSide; - final bool isEditing; - final bool isNew; - - @override - State createState() => _GridFieldCellState(); -} - -class _GridFieldCellState extends State { - final PopoverController popoverController = PopoverController(); - late final FieldCellBloc _bloc; - - @override - void initState() { - super.initState(); - _bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo); - if (widget.isEditing) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - popoverController.show(); - }); - } - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) { - _bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo)); - } - if (widget.isEditing) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - popoverController.show(); - }); - } - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _bloc, - child: BlocBuilder( - builder: (context, state) { - final button = AppFlowyPopover( - triggerActions: PopoverTriggerFlags.none, - constraints: const BoxConstraints(), - margin: EdgeInsets.zero, - direction: PopoverDirection.bottomWithLeftAligned, - controller: popoverController, - popupBuilder: (BuildContext context) { - widget.onEditorOpened(); - return FieldEditor( - viewId: widget.viewId, - fieldController: widget.fieldController, - fieldInfo: widget.fieldInfo, - isNewField: widget.isNew, - initialPage: widget.isNew - ? FieldEditorPage.details - : FieldEditorPage.general, - onFieldInserted: widget.onFieldInsertedOnEitherSide, - ); - }, - child: SizedBox( - height: GridSize.headerHeight, - child: FieldCellButton( - field: widget.fieldInfo.field, - onTap: widget.onTap, - margin: const EdgeInsetsDirectional.fromSTEB(12, 9, 10, 9), - ), - ), - ); - - const line = Positioned( - top: 0, - bottom: 0, - right: 0, - child: DragToExpandLine(), - ); - - return _GridHeaderCellContainer( - width: state.width, - child: Stack( - alignment: Alignment.centerRight, - children: [button, line], - ), - ); - }, - ), - ); - } - - @override - void dispose() { - _bloc.close(); - super.dispose(); - } -} - -class _GridHeaderCellContainer extends StatelessWidget { - const _GridHeaderCellContainer({ - required this.child, - required this.width, - }); - - final Widget child; - final double width; - - @override - Widget build(BuildContext context) { - final borderSide = - BorderSide(color: AFThemeExtension.of(context).borderColor); - final decoration = BoxDecoration( - border: Border( - right: borderSide, - bottom: borderSide, - ), - ); - - return Container( - width: width, - decoration: decoration, - child: child, - ); - } -} - -@visibleForTesting -class DragToExpandLine extends StatelessWidget { - const DragToExpandLine({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () {}, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onHorizontalDragStart: (details) { - context - .read() - .add(const FieldCellEvent.onResizeStart()); - }, - onHorizontalDragUpdate: (value) { - context - .read() - .add(FieldCellEvent.startUpdateWidth(value.localPosition.dx)); - }, - onHorizontalDragEnd: (end) { - context - .read() - .add(const FieldCellEvent.endUpdateWidth()); - }, - child: FlowyHover( - cursor: SystemMouseCursors.resizeLeftRight, - style: HoverStyle( - hoverColor: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.zero, - contentMargin: const EdgeInsets.only(left: 6), - ), - child: const SizedBox(width: 4), - ), - ), - ); - } -} - -class FieldCellButton extends StatelessWidget { - const FieldCellButton({ - super.key, - required this.field, - required this.onTap, - this.maxLines = 1, - this.radius = BorderRadius.zero, - this.margin, - }); - - final FieldPB field; - final VoidCallback onTap; - final int? maxLines; - final BorderRadius? radius; - final EdgeInsetsGeometry? margin; - - @override - Widget build(BuildContext context) { - return FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: onTap, - leftIcon: FieldIcon( - fieldInfo: FieldInfo.initial(field), - ), - rightIcon: field.fieldType.rightIcon != null - ? FlowySvg( - field.fieldType.rightIcon!, - blendMode: null, - size: const Size.square(18), - ) - : null, - radius: radius, - text: FlowyText( - field.name, - lineHeight: 1.0, - maxLines: maxLines, - overflow: TextOverflow.ellipsis, - color: AFThemeExtension.of(context).textColor, - ), - margin: margin ?? GridSize.cellContentInsets, - ); - } -} - -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 deleted file mode 100644 index c59327113b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_extension.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; - -extension RowDetailAccessoryExtension on FieldType { - bool get showRowDetailAccessory => switch (this) { - FieldType.Media => false, - _ => true, - }; -} 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 deleted file mode 100644 index e30c238f96..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ /dev/null @@ -1,219 +0,0 @@ -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/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: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:reorderables/reorderables.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../layout/sizes.dart'; -import 'desktop_field_cell.dart'; - -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() => - _GridHeaderSliverAdaptorState(); -} - -class _GridHeaderSliverAdaptorState extends State { - @override - Widget build(BuildContext context) { - final fieldController = - context.read().databaseController.fieldController; - final horizontalPadding = - context.read()?.horizontalPadding ?? - 0.0; - return BlocProvider( - create: (context) { - return GridHeaderBloc( - viewId: widget.viewId, - fieldController: fieldController, - )..add(const GridHeaderEvent.initial()); - }, - 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, - ), - ), - ), - ); - } -} - -class _GridHeader extends StatefulWidget { - const _GridHeader({required this.viewId, required this.fieldController}); - - final String viewId; - final FieldController fieldController; - - @override - State<_GridHeader> createState() => _GridHeaderState(); -} - -class _GridHeaderState extends State<_GridHeader> { - final Map> _gridMap = {}; - final _scrollController = ScrollController(); - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final cells = state.fields - .map( - (fieldInfo) => GridFieldCell( - key: _getKeyById(fieldInfo.id), - viewId: widget.viewId, - fieldInfo: fieldInfo, - fieldController: widget.fieldController, - onTap: () => context - .read() - .add(GridHeaderEvent.startEditingField(fieldInfo.id)), - onFieldInsertedOnEitherSide: (fieldId) => context - .read() - .add(GridHeaderEvent.startEditingNewField(fieldId)), - onEditorOpened: () => context - .read() - .add(const GridHeaderEvent.endEditingField()), - isEditing: state.editingFieldId == fieldInfo.id, - isNew: state.newFieldId == fieldInfo.id, - ), - ) - .toList(); - - return RepaintBoundary( - child: ReorderableRow( - scrollController: _scrollController, - buildDraggableFeedback: (context, constraints, child) => Material( - color: Colors.transparent, - child: child, - ), - draggingWidgetOpacity: 0, - header: _cellLeading(), - needsLongPressDraggable: UniversalPlatform.isMobile, - footer: _CellTrailing(viewId: widget.viewId), - onReorder: (int oldIndex, int newIndex) { - context - .read() - .add(GridHeaderEvent.moveField(oldIndex, newIndex)); - }, - children: cells, - ), - ); - }, - ); - } - - /// This is a workaround for [ReorderableRow]. - /// [ReorderableRow] warps the child's key with a [GlobalKey]. - /// It will trigger the child's widget's to recreate. - /// The state will lose. - ValueKey? _getKeyById(String id) { - if (_gridMap.containsKey(id)) { - return _gridMap[id]; - } - final newKey = ValueKey(id); - _gridMap[id] = newKey; - return newKey; - } - - Widget _cellLeading() { - return SizedBox( - width: context.read().horizontalPadding, - ); - } -} - -class _CellTrailing extends StatelessWidget { - const _CellTrailing({required this.viewId}); - - final String viewId; - - @override - Widget build(BuildContext context) { - return Container( - width: GridSize.newPropertyButtonWidth, - height: GridSize.headerHeight, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: AFThemeExtension.of(context).borderColor), - ), - ), - child: CreateFieldButton( - viewId: viewId, - onFieldCreated: (fieldId) => context - .read() - .add(GridHeaderEvent.startEditingNewField(fieldId)), - ), - ); - } -} - -class CreateFieldButton extends StatelessWidget { - const CreateFieldButton({ - super.key, - required this.viewId, - required this.onFieldCreated, - }); - - final String viewId; - final void Function(String fieldId) onFieldCreated; - - @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, - ); - result.fold( - (field) => 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), - ), - ); - } -} 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 deleted file mode 100755 index 20cc030ff4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_field_button.dart +++ /dev/null @@ -1,83 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileFieldButton extends StatelessWidget { - const MobileFieldButton.first({ - super.key, - required this.viewId, - required this.fieldController, - required this.fieldInfo, - }) : radius = const BorderRadius.only(topLeft: Radius.circular(24)), - margin = const EdgeInsets.symmetric(vertical: 14, horizontal: 18), - index = null; - - const MobileFieldButton({ - super.key, - required this.viewId, - required this.fieldController, - required this.fieldInfo, - required this.index, - }) : radius = BorderRadius.zero, - margin = const EdgeInsets.symmetric(vertical: 14, horizontal: 12); - - final String viewId; - final int? index; - final FieldController fieldController; - final FieldInfo fieldInfo; - final BorderRadius? radius; - final EdgeInsets? margin; - - @override - Widget build(BuildContext context) { - Widget child = Container( - width: 200, - decoration: _getDecoration(context), - child: FlowyButton( - onTap: () => - showQuickEditField(context, viewId, fieldController, fieldInfo), - radius: radius, - margin: margin, - leftIconSize: const Size.square(18), - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - dimension: 18, - ), - text: FlowyText( - fieldInfo.name, - fontSize: 15, - overflow: TextOverflow.ellipsis, - ), - ), - ); - - if (index != null) { - child = ReorderableDelayedDragStartListener(index: index!, child: child); - } - - return child; - } - - BoxDecoration? _getDecoration(BuildContext context) { - final borderSide = BorderSide( - color: Theme.of(context).dividerColor, - ); - - if (index == null) { - return BoxDecoration( - borderRadius: const BorderRadiusDirectional.only( - topStart: Radius.circular(24), - ), - border: BorderDirectional( - top: borderSide, - start: borderSide, - ), - ); - } else { - return null; - } - } -} 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 deleted file mode 100644 index 369bdeb523..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.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/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'; -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 '../../mobile_grid_page.dart'; -import 'mobile_field_button.dart'; - -const double _kGridHeaderHeight = 50.0; - -class MobileGridHeader extends StatefulWidget { - const MobileGridHeader({ - super.key, - required this.viewId, - required this.contentScrollController, - required this.reorderableController, - }); - - final String viewId; - final ScrollController contentScrollController; - final ScrollController reorderableController; - - @override - State createState() => _MobileGridHeaderState(); -} - -class _MobileGridHeaderState extends State { - @override - Widget build(BuildContext context) { - final fieldController = - context.read().databaseController.fieldController; - final isLocked = - context.read()?.state.isLocked ?? false; - return BlocProvider( - create: (context) { - return GridHeaderBloc( - viewId: widget.viewId, - fieldController: fieldController, - )..add(const GridHeaderEvent.initial()); - }, - child: Stack( - children: [ - BlocBuilder( - builder: (context, state) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: widget.contentScrollController, - child: Stack( - children: [ - Positioned( - top: 0, - left: GridSize.horizontalHeaderPadding + 24, - right: GridSize.horizontalHeaderPadding + 24, - child: _divider(), - ), - Positioned( - bottom: 0, - left: GridSize.horizontalHeaderPadding, - right: GridSize.horizontalHeaderPadding, - child: _divider(), - ), - SizedBox( - height: _kGridHeaderHeight, - width: getMobileGridContentWidth(state.fields), - ), - ], - ), - ); - }, - ), - IgnorePointer( - ignoring: isLocked, - child: SizedBox( - height: _kGridHeaderHeight, - child: _GridHeader( - viewId: widget.viewId, - fieldController: fieldController, - scrollController: widget.reorderableController, - ), - ), - ), - ], - ), - ); - } - - Widget _divider() { - return Divider( - height: 1, - thickness: 1, - color: Theme.of(context).dividerColor, - ); - } -} - -class _GridHeader extends StatefulWidget { - const _GridHeader({ - required this.viewId, - required this.fieldController, - required this.scrollController, - }); - - final String viewId; - final FieldController fieldController; - final ScrollController scrollController; - - @override - State<_GridHeader> createState() => _GridHeaderState(); -} - -class _GridHeaderState extends State<_GridHeader> { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final fields = [...state.fields]; - FieldInfo? firstField; - if (fields.isNotEmpty) { - firstField = fields.removeAt(0); - } - - final cells = fields - .mapIndexed( - (index, fieldInfo) => MobileFieldButton( - key: ValueKey(fieldInfo.id), - index: index, - viewId: widget.viewId, - fieldController: widget.fieldController, - fieldInfo: fieldInfo, - ), - ) - .toList(); - - return ReorderableListView.builder( - scrollController: widget.scrollController, - shrinkWrap: true, - scrollDirection: Axis.horizontal, - proxyDecorator: (child, index, anim) => Material( - color: Colors.transparent, - child: child, - ), - padding: EdgeInsets.symmetric( - horizontal: GridSize.horizontalHeaderPadding, - ), - header: firstField != null - ? MobileFieldButton.first( - viewId: widget.viewId, - fieldController: widget.fieldController, - fieldInfo: firstField, - ) - : null, - footer: CreateFieldButton( - viewId: widget.viewId, - onFieldCreated: (fieldId) => context - .read() - .add(GridHeaderEvent.startEditingNewField(fieldId)), - ), - onReorder: (int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - newIndex--; - } - oldIndex++; - newIndex++; - context - .read() - .add(GridHeaderEvent.moveField(oldIndex, newIndex)); - }, - itemCount: cells.length, - itemBuilder: (context, index) => cells[index], - ); - }, - ); - } -} - -class CreateFieldButton extends StatelessWidget { - const CreateFieldButton({ - super.key, - required this.viewId, - required this.onFieldCreated, - }); - - final String viewId; - final void Function(String fieldId) onFieldCreated; - - @override - Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints( - maxWidth: GridSize.mobileNewPropertyButtonWidth, - minHeight: GridSize.headerHeight, - ), - decoration: _getDecoration(context), - child: FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), - radius: const BorderRadius.only(topRight: Radius.circular(24)), - text: FlowyText( - LocaleKeys.grid_field_newProperty.tr(), - fontSize: 15, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, - ), - hoverColor: AFThemeExtension.of(context).greyHover, - onTap: () => mobileCreateFieldWorkflow(context, viewId), - leftIconSize: const Size.square(18), - leftIcon: FlowySvg( - FlowySvgs.add_s, - size: const Size.square(18), - color: Theme.of(context).hintColor, - ), - ), - ); - } - - BoxDecoration? _getDecoration(BuildContext context) { - final borderSide = BorderSide( - color: Theme.of(context).dividerColor, - ); - - return BoxDecoration( - borderRadius: const BorderRadiusDirectional.only( - topEnd: Radius.circular(24), - ), - border: BorderDirectional( - top: borderSide, - end: borderSide, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart deleted file mode 100644 index 7c18bb927f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; -import 'package:appflowy/plugins/database/grid/application/grid_bloc.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'; - -Widget getGridFabs(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - MobileGridFab( - backgroundColor: Theme.of(context).colorScheme.surface, - foregroundColor: Theme.of(context).primaryColor, - onTap: () { - final bloc = context.read(); - if (bloc.state.rowInfos.isNotEmpty) { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: bloc.state.rowInfos.first.rowId, - MobileRowDetailPage.argDatabaseController: - bloc.databaseController, - }, - ); - } - }, - boxShadow: const BoxShadow( - offset: Offset(0, 8), - color: Color(0x145D7D8B), - blurRadius: 20, - ), - icon: FlowySvgs.properties_s, - iconSize: const Size.square(24), - ), - const HSpace(16), - MobileGridFab( - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.white, - onTap: () { - context - .read() - .add(const GridEvent.createRow(openRowDetail: true)); - }, - overlayColor: const WidgetStatePropertyAll(Color(0xFF009FD1)), - boxShadow: const BoxShadow( - offset: Offset(0, 8), - color: Color(0x6612BFEF), - blurRadius: 18, - spreadRadius: -5, - ), - icon: FlowySvgs.add_s, - iconSize: const Size.square(24), - ), - ], - ); -} - -class MobileGridFab extends StatelessWidget { - const MobileGridFab({ - super.key, - required this.backgroundColor, - required this.foregroundColor, - required this.boxShadow, - required this.onTap, - required this.icon, - required this.iconSize, - this.overlayColor, - }); - - final Color backgroundColor; - final Color foregroundColor; - final BoxShadow boxShadow; - final VoidCallback onTap; - final FlowySvgData icon; - final Size iconSize; - final WidgetStateProperty? overlayColor; - - @override - Widget build(BuildContext context) { - final radius = BorderRadius.circular(20); - return DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - border: const Border.fromBorderSide( - BorderSide(width: 0.5, color: Color(0xFFE4EDF0)), - ), - borderRadius: radius, - boxShadow: [boxShadow], - ), - child: Material( - borderOnForeground: false, - color: Colors.transparent, - borderRadius: radius, - child: InkWell( - borderRadius: radius, - overlayColor: overlayColor, - onTap: onTap, - child: SizedBox.square( - dimension: 56, - child: Center( - child: FlowySvg( - icon, - color: foregroundColor, - size: iconSize, - ), - ), - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index d212c50746..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart +++ /dev/null @@ -1,144 +0,0 @@ -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: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({ - super.key, - required this.viewId, - required this.rowId, - this.actions = RowAction.values, - this.groupId, - }); - - const RowActionMenu.board({ - super.key, - required this.viewId, - required this.rowId, - required this.groupId, - }) : actions = const [RowAction.duplicate, RowAction.delete]; - - final String viewId; - final RowId rowId; - final List actions; - final String? groupId; - - @override - Widget build(BuildContext context) { - final cells = - actions.map((action) => _actionCell(context, action)).toList(); - - return SeparatedColumn( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => VSpace(GridSize.typeOptionSeparatorHeight), - children: cells, - ); - } - - Widget _actionCell(BuildContext context, RowAction action) { - Widget icon = FlowySvg(action.icon); - if (action == RowAction.insertAbove) { - icon = RotatedBox(quarterTurns: 1, child: icon); - } - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - action.text, - overflow: TextOverflow.ellipsis, - lineHeight: 1.0, - ), - onTap: () { - action.performAction(context, viewId, rowId); - PopoverContainer.of(context).close(); - }, - leftIcon: icon, - ), - ); - } -} - -enum RowAction { - insertAbove, - insertBelow, - duplicate, - delete; - - FlowySvgData get icon { - return switch (this) { - insertAbove => FlowySvgs.arrow_s, - insertBelow => FlowySvgs.add_s, - duplicate => FlowySvgs.duplicate_s, - delete => FlowySvgs.delete_s, - }; - } - - String get text { - return switch (this) { - insertAbove => LocaleKeys.grid_row_insertRecordAbove.tr(), - insertBelow => LocaleKeys.grid_row_insertRecordBelow.tr(), - duplicate => LocaleKeys.grid_row_duplicate.tr(), - delete => LocaleKeys.grid_row_delete.tr(), - }; - } - - void performAction(BuildContext context, String viewId, String rowId) { - switch (this) { - case insertAbove: - case insertBelow: - 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, - ); - } - 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]), - ); - 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 deleted file mode 100755 index 209c439ad1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:flutter/material.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/application/row/row_cache.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/grid/application/row/row_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/mobile_cell_container.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../layout/sizes.dart'; - -class MobileGridRow extends StatefulWidget { - const MobileGridRow({ - super.key, - required this.rowId, - required this.databaseController, - required this.openDetailPage, - this.isDraggable = false, - }); - - final RowId rowId; - final DatabaseController databaseController; - final void Function(BuildContext context) openDetailPage; - final bool isDraggable; - - @override - State createState() => _MobileGridRowState(); -} - -class _MobileGridRowState extends State { - late final RowController _rowController; - late final EditableCellBuilder _cellBuilder; - - String get viewId => widget.databaseController.viewId; - RowCache get rowCache => widget.databaseController.rowCache; - - @override - void initState() { - super.initState(); - _rowController = RowController( - rowMeta: rowCache.getRow(widget.rowId)!.rowMeta, - viewId: viewId, - rowCache: rowCache, - ); - _cellBuilder = EditableCellBuilder( - databaseController: widget.databaseController, - ); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RowBloc( - fieldController: widget.databaseController.fieldController, - rowId: widget.rowId, - rowController: _rowController, - viewId: viewId, - ), - child: BlocBuilder( - builder: (context, state) { - return Row( - children: [ - SizedBox(width: GridSize.horizontalHeaderPadding), - Expanded( - child: RowContent( - fieldController: widget.databaseController.fieldController, - builder: _cellBuilder, - onExpand: () => widget.openDetailPage(context), - ), - ), - ], - ); - }, - ), - ); - } - - @override - Future dispose() async { - super.dispose(); - await _rowController.dispose(); - } -} - -class RowContent extends StatelessWidget { - const RowContent({ - super.key, - required this.fieldController, - required this.onExpand, - required this.builder, - }); - - final FieldController fieldController; - final VoidCallback onExpand; - final EditableCellBuilder builder; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return SizedBox( - height: 52, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ..._makeCells(context, state.cellContexts), - _finalCellDecoration(context), - ], - ), - ); - }, - ); - } - - List _makeCells( - BuildContext context, - List cellContexts, - ) { - return cellContexts.map( - (cellContext) { - final fieldInfo = fieldController.getField(cellContext.fieldId)!; - final EditableCellWidget child = builder.buildStyled( - cellContext, - EditableCellStyle.mobileGrid, - ); - return MobileCellContainer( - isPrimary: fieldInfo.field.isPrimary, - onPrimaryFieldCellTap: onExpand, - child: child, - ); - }, - ).toList(); - } - - Widget _finalCellDecoration(BuildContext context) { - return Container( - width: 200, - constraints: const BoxConstraints(minHeight: 46), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor), - right: BorderSide(color: Theme.of(context).dividerColor), - ), - ), - ); - } -} 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 deleted file mode 100755 index 2306767f46..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ /dev/null @@ -1,379 +0,0 @@ -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: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 { - const GridRow({ - super.key, - required this.fieldController, - required this.viewId, - required this.rowId, - required this.rowController, - required this.cellBuilder, - required this.openDetailPage, - required this.index, - this.shrinkWrap = false, - required this.editable, - }); - - final FieldController fieldController; - final String viewId; - final RowId rowId; - final RowController rowController; - final EditableCellBuilder cellBuilder; - final void Function(BuildContext context) openDetailPage; - final int index; - final bool shrinkWrap; - final bool editable; - - @override - Widget build(BuildContext context) { - Widget rowContent = RowContent( - fieldController: fieldController, - cellBuilder: cellBuilder, - onExpand: () => openDetailPage(context), - ); - - if (!shrinkWrap) { - rowContent = Expanded(child: rowContent); - } - - rowContent = BlocProvider( - create: (_) => RowBloc( - fieldController: fieldController, - rowId: rowId, - rowController: rowController, - viewId: viewId, - ), - child: _RowEnterRegion( - child: Row( - children: [ - _RowLeading(viewId: viewId, index: index), - rowContent, - ], - ), - ), - ); - - if (!editable) { - rowContent = IgnorePointer( - child: rowContent, - ); - } - - return rowContent; - } -} - -class _RowLeading extends StatefulWidget { - const _RowLeading({ - required this.viewId, - required this.index, - }); - - final String viewId; - final int index; - - @override - State<_RowLeading> createState() => _RowLeadingState(); -} - -class _RowLeadingState extends State<_RowLeading> { - final PopoverController popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - constraints: BoxConstraints.loose(const Size(200, 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, - ), - ); - }, - child: Consumer( - builder: (context, state, _) { - return SizedBox( - width: context - .read() - .horizontalPadding, - child: state.onEnter ? _activeWidget() : null, - ); - }, - ), - ); - } - - Widget _activeWidget() { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - InsertRowButton(viewId: widget.viewId), - ReorderableDragStartListener( - index: widget.index, - child: RowMenuButton( - openMenu: popoverController.show, - ), - ), - ], - ); - } -} - -class InsertRowButton extends StatelessWidget { - const InsertRowButton({ - super.key, - required this.viewId, - }); - - final String viewId; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - tooltipText: LocaleKeys.tooltip_addNewRow.tr(), - 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()); - } - }, - iconPadding: const EdgeInsets.all(3), - icon: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).colorScheme.tertiary, - ), - ); - } -} - -class RowMenuButton extends StatefulWidget { - const RowMenuButton({ - super.key, - required this.openMenu, - }); - - final VoidCallback openMenu; - - @override - State createState() => _RowMenuButtonState(); -} - -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(), - ), - ], - ), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - width: 20, - height: 30, - onPressed: () => widget.openMenu(), - iconPadding: const EdgeInsets.all(3), - icon: FlowySvg( - FlowySvgs.drag_element_s, - color: Theme.of(context).colorScheme.tertiary, - ), - ); - } -} - -class RowContent extends StatelessWidget { - const RowContent({ - super.key, - required this.fieldController, - required this.cellBuilder, - required this.onExpand, - }); - - final FieldController fieldController; - final VoidCallback onExpand; - final EditableCellBuilder cellBuilder; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ..._makeCells(context, state.cellContexts), - _finalCellDecoration(context), - ], - ), - ); - }, - ); - } - - List _makeCells( - BuildContext context, - List cellContexts, - ) { - return cellContexts.map( - (cellContext) { - final fieldInfo = fieldController.getField(cellContext.fieldId)!; - final EditableCellWidget child = cellBuilder.buildStyled( - cellContext, - EditableCellStyle.desktopGrid, - ); - return CellContainer( - width: fieldInfo.width!.toDouble(), - isPrimary: fieldInfo.field.isPrimary, - accessoryBuilder: (buildContext) { - final builder = child.accessoryBuilder; - final List accessories = []; - if (fieldInfo.field.isPrimary) { - accessories.add( - GridCellAccessoryBuilder( - builder: (key) => PrimaryCellAccessory( - key: key, - onTap: onExpand, - isCellEditing: buildContext.isCellEditing, - ), - ), - ); - } - - if (builder != null) { - accessories.addAll(builder(buildContext)); - } - - return accessories; - }, - child: child, - ); - }, - ).toList(); - } - - 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), - ), - ), - ); - }, - ), - ); - } -} - -class RegionStateNotifier extends ChangeNotifier { - bool _onEnter = false; - - set onEnter(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get onEnter => _onEnter; -} - -class _RowEnterRegion extends StatefulWidget { - const _RowEnterRegion({required this.child}); - - final Widget child; - - @override - State<_RowEnterRegion> createState() => _RowEnterRegionState(); -} - -class _RowEnterRegionState extends State<_RowEnterRegion> { - late final RegionStateNotifier _rowStateNotifier; - - @override - void initState() { - super.initState(); - _rowStateNotifier = RegionStateNotifier(); - } - - @override - Future dispose() async { - _rowStateNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: _rowStateNotifier, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (p) => _rowStateNotifier.onEnter = true, - onExit: (p) => _rowStateNotifier.onEnter = false, - child: widget.child, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart deleted file mode 100644 index 7d743702fc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class GridShortcuts extends StatelessWidget { - const GridShortcuts({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Shortcuts( - shortcuts: bindKeys([]), - child: Actions( - dispatcher: LoggingActionDispatcher(), - actions: const {}, - child: child, - ), - ); - } -} - -Map bindKeys(List keys) { - return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)}; -} - -class KeyboardKeyIdent extends Intent { - const KeyboardKeyIdent(this.key); - - final KeyboardKey key; -} - -class LoggingActionDispatcher extends ActionDispatcher { - @override - Object? invokeAction( - covariant Action action, - covariant Intent intent, [ - BuildContext? context, - ]) { - // print('Action invoked: $action($intent) from $context'); - super.invokeAction(action, intent, context); - - return null; - } -} 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 deleted file mode 100644 index 5218a60ee5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart +++ /dev/null @@ -1,147 +0,0 @@ -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: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/scrolling/styled_list.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class CreateDatabaseViewSortList extends StatelessWidget { - const CreateDatabaseViewSortList({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @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(); - - 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( - shrinkWrap: true, - slivers: slivers, - physics: StyledScrollPhysics(), - ); - }, - ), - ), - ); - } -} - -class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate { - _SortTextFieldDelegate(); - - double fixHeight = 36; - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return Container( - padding: const EdgeInsets.only(bottom: 4), - color: Theme.of(context).cardColor, - height: fixHeight, - child: FlowyTextField( - hintText: LocaleKeys.grid_settings_sortBy.tr(), - onChanged: (text) { - context - .read>() - .add(SimpleTextFilterEvent.updateFilter(text)); - }, - ), - ); - } - - @override - double get maxExtent => fixHeight; - - @override - double get minExtent => fixHeight; - - @override - bool shouldRebuild(covariant oldDelegate) => false; -} - -class GridSortPropertyCell extends StatelessWidget { - const GridSortPropertyCell({ - super.key, - required this.fieldInfo, - required this.onTap, - }); - - final FieldInfo fieldInfo; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - fieldInfo.name, - lineHeight: 1.0, - color: AFThemeExtension.of(context).textColor, - ), - onTap: onTap, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - ), - ), - ); - } -} 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 deleted file mode 100644 index 9bf8f36c85..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/order_panel.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_editor.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.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 OrderPanel extends StatelessWidget { - const OrderPanel({required this.onCondition, super.key}); - - final Function(SortConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - final List children = SortConditionPB.values.map((condition) { - return OrderPanelItem( - condition: condition, - onCondition: onCondition, - ); - }).toList(); - - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: 160), - child: IntrinsicWidth( - child: IntrinsicHeight( - child: Column( - children: children, - ), - ), - ), - ); - } -} - -class OrderPanelItem extends StatelessWidget { - const OrderPanelItem({ - super.key, - required this.condition, - required this.onCondition, - }); - - final SortConditionPB condition; - final Function(SortConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText(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 deleted file mode 100644 index 4d509b3862..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart +++ /dev/null @@ -1,51 +0,0 @@ -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'; - -class SortChoiceButton extends StatelessWidget { - const SortChoiceButton({ - super.key, - required this.text, - this.onTap, - this.radius = const Radius.circular(14), - this.leftIcon, - this.rightIcon, - this.editable = true, - }); - - final String text; - final VoidCallback? onTap; - final Radius radius; - final Widget? leftIcon; - final Widget? rightIcon; - final bool editable; - - @override - Widget build(BuildContext context) { - return FlowyButton( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: BorderRadius.all(radius), - ), - useIntrinsicWidth: true, - text: FlowyText( - text, - lineHeight: 1.0, - color: AFThemeExtension.of(context).textColor, - overflow: TextOverflow.ellipsis, - ), - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - radius: BorderRadius.all(radius), - leftIcon: leftIcon, - rightIcon: rightIcon, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: onTap, - disable: !editable, - disableOpacity: 1.0, - ); - } -} 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 deleted file mode 100644 index 2f7f68e2f6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart +++ /dev/null @@ -1,314 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'create_sort_list.dart'; -import 'order_panel.dart'; -import 'sort_choice_button.dart'; - -class SortEditor extends StatefulWidget { - const SortEditor({super.key}); - - @override - State createState() => _SortEditorState(); -} - -class _SortEditorState extends State { - final popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return ReorderableListView.builder( - onReorder: (oldIndex, newIndex) => context - .read() - .add(SortEditorEvent.reorderSort(oldIndex, newIndex)), - itemCount: state.sorts.length, - itemBuilder: (context, index) => DatabaseSortItem( - key: ValueKey(state.sorts[index].sortId), - index: index, - sort: state.sorts[index], - popoverMutex: popoverMutex, - ), - proxyDecorator: (child, index, animation) => 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(), - ), - ], - ), - ), - shrinkWrap: true, - buildDefaultDragHandles: false, - footer: Row( - children: [ - Flexible( - child: DatabaseAddSortButton( - disable: state.creatableFields.isEmpty, - popoverMutex: popoverMutex, - ), - ), - const HSpace(6), - Flexible( - child: DeleteAllSortsButton( - popoverMutex: popoverMutex, - ), - ), - ], - ), - ); - }, - ); - } -} - -class DatabaseSortItem extends StatelessWidget { - const DatabaseSortItem({ - super.key, - required this.index, - required this.popoverMutex, - required this.sort, - }); - - final int index; - final PopoverMutex popoverMutex; - final DatabaseSort sort; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 6), - color: Theme.of(context).cardColor, - child: Row( - children: [ - ReorderableDragStartListener( - index: index, - child: MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: SizedBox( - width: 14 + 12, - height: 14, - child: FlowySvg( - FlowySvgs.drag_element_s, - size: const Size.square(14), - color: Theme.of(context).iconTheme.color, - ), - ), - ), - ), - Flexible( - 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, - ); - }, - ), - ), - ), - const HSpace(6), - Flexible( - fit: FlexFit.tight, - child: SizedBox( - height: 26, - child: SortConditionButton( - sort: sort, - popoverMutex: popoverMutex, - ), - ), - ), - const HSpace(6), - FlowyIconButton( - width: 26, - onPressed: () { - context - .read() - .add(SortEditorEvent.deleteSort(sort.sortId)); - PopoverContainer.of(context).close(); - }, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - icon: FlowySvg( - FlowySvgs.trash_m, - color: Theme.of(context).iconTheme.color, - size: const Size.square(16), - ), - ), - ], - ), - ); - } -} - -extension SortConditionExtension on SortConditionPB { - String get title { - return switch (this) { - SortConditionPB.Ascending => LocaleKeys.grid_sort_ascending.tr(), - SortConditionPB.Descending => LocaleKeys.grid_sort_descending.tr(), - _ => throw UnimplementedError(), - }; - } -} - -class DatabaseAddSortButton extends StatefulWidget { - const DatabaseAddSortButton({ - super.key, - required this.disable, - required this.popoverMutex, - }); - - final bool disable; - final PopoverMutex popoverMutex; - - @override - State createState() => _DatabaseAddSortButtonState(); -} - -class _DatabaseAddSortButtonState extends State { - final _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: _popoverController, - mutex: widget.popoverMutex, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(200, 300)), - offset: const Offset(-6, 8), - triggerActions: PopoverTriggerFlags.none, - asBarrier: true, - popupBuilder: (popoverContext) { - return BlocProvider.value( - value: context.read(), - child: CreateDatabaseViewSortList( - onTap: () => _popoverController.close(), - ), - ); - }, - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).greyHover, - disable: widget.disable, - text: FlowyText(LocaleKeys.grid_sort_addSort.tr()), - onTap: () => _popoverController.show(), - leftIcon: const FlowySvg(FlowySvgs.add_s), - ), - ), - ); - } -} - -class DeleteAllSortsButton extends StatelessWidget { - const DeleteAllSortsButton({super.key, required this.popoverMutex}); - - final PopoverMutex popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText(LocaleKeys.grid_sort_deleteAllSorts.tr()), - onTap: () { - context - .read() - .add(const SortEditorEvent.deleteAllSorts()); - PopoverContainer.of(context).close(); - }, - leftIcon: const FlowySvg(FlowySvgs.delete_s), - ), - ); - }, - ); - } -} - -class SortConditionButton extends StatefulWidget { - const SortConditionButton({ - super.key, - required this.popoverMutex, - required this.sort, - }); - - final PopoverMutex popoverMutex; - final DatabaseSort sort; - - @override - State createState() => _SortConditionButtonState(); -} - -class _SortConditionButtonState extends State { - final PopoverController popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - mutex: widget.popoverMutex, - constraints: BoxConstraints.loose(const Size(340, 200)), - direction: PopoverDirection.bottomWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - popupBuilder: (BuildContext popoverContext) { - return OrderPanel( - onCondition: (condition) { - context.read().add( - SortEditorEvent.editSort( - sortId: widget.sort.sortId, - condition: condition, - ), - ); - popoverController.close(); - }, - ); - }, - child: SortChoiceButton( - text: widget.sort.condition.title, - rightIcon: FlowySvg( - FlowySvgs.arrow_down_s, - color: Theme.of(context).iconTheme.color, - ), - onTap: () => popoverController.show(), - ), - ); - } -} 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 deleted file mode 100644 index fc35e76241..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart +++ /dev/null @@ -1,93 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'sort_choice_button.dart'; -import 'sort_editor.dart'; - -class SortMenu extends StatelessWidget { - const SortMenu({ - super.key, - required this.fieldController, - }); - - final FieldController fieldController; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SortEditorBloc( - viewId: fieldController.viewId, - fieldController: fieldController, - ), - child: BlocBuilder( - builder: (context, state) { - if (state.sorts.isEmpty) { - return const SizedBox.shrink(); - } - - return AppFlowyPopover( - controller: PopoverController(), - constraints: BoxConstraints.loose(const Size(320, 200)), - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 5), - margin: const EdgeInsets.fromLTRB(6.0, 0.0, 6.0, 6.0), - popupBuilder: (BuildContext popoverContext) { - return BlocProvider.value( - value: context.read(), - child: const SortEditor(), - ); - }, - child: SortChoiceChip(sorts: state.sorts), - ); - }, - ), - ); - } -} - -class SortChoiceChip extends StatelessWidget { - const SortChoiceChip({ - super.key, - required this.sorts, - this.onTap, - }); - - final List sorts; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - final arrow = Transform.rotate( - angle: -math.pi / 2, - child: FlowySvg( - FlowySvgs.arrow_left_s, - color: Theme.of(context).iconTheme.color, - ), - ); - - final text = LocaleKeys.grid_settings_sort.tr(); - final leftIcon = FlowySvg( - FlowySvgs.sort_ascending_s, - color: Theme.of(context).iconTheme.color, - ); - - return SizedBox( - height: 28, - child: SortChoiceButton( - text: text, - leftIcon: leftIcon, - rightIcon: arrow, - onTap: onTap, - ), - ); - } -} 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 deleted file mode 100644 index 5c33426281..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart +++ /dev/null @@ -1,80 +0,0 @@ -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: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 '../filter/create_filter_list.dart'; - -class FilterButton extends StatefulWidget { - const FilterButton({ - super.key, - required this.toggleExtension, - }); - - final ToggleExtensionNotifier toggleExtension; - - @override - State createState() => _FilterButtonState(); -} - -class _FilterButtonState extends State { - final _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - 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(); - } - }, - ), - ), - ); - }, - ); - } - - Widget _wrapPopover(Widget child) { - return AppFlowyPopover( - controller: _popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(200, 300)), - 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(); - }, - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index f325ab206f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart +++ /dev/null @@ -1,72 +0,0 @@ -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/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({ - super.key, - required this.controller, - required this.toggleExtension, - }); - - final DatabaseController controller; - final ToggleExtensionNotifier toggleExtension; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => FilterEditorBloc( - viewId: controller.viewId, - fieldController: controller.fieldController, - ), - ), - 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) ...[ - const HSpace(2), - ViewDatabaseButton(view: controller.view), - ], - 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 deleted file mode 100644 index 6649d53594..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart +++ /dev/null @@ -1,76 +0,0 @@ -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: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 '../sort/create_sort_list.dart'; - -class SortButton extends StatefulWidget { - const SortButton({super.key, required this.toggleExtension}); - - final ToggleExtensionNotifier toggleExtension; - - @override - State createState() => _SortButtonState(); -} - -class _SortButtonState extends State { - final _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - 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(); - } - }, - ), - ), - ); - }, - ); - } - - Widget wrapPopover(Widget child) { - return AppFlowyPopover( - controller: _popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(200, 300)), - offset: const Offset(0, 8), - triggerActions: PopoverTriggerFlags.none, - popupBuilder: (popoverContext) { - return BlocProvider.value( - value: context.read(), - child: CreateDatabaseViewSortList( - onTap: () { - if (!widget.toggleExtension.isToggled) { - widget.toggleExtension.toggle(); - } - _popoverController.close(); - }, - ), - ); - }, - 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 deleted file mode 100644 index 69e5d27d37..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart +++ /dev/null @@ -1,81 +0,0 @@ -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/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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; - -class DatabaseViewSettingExtension extends StatelessWidget { - const DatabaseViewSettingExtension({ - super.key, - required this.viewId, - required this.databaseController, - required this.toggleExtension, - }); - - final String viewId; - final DatabaseController databaseController; - final ToggleExtensionNotifier toggleExtension; - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: toggleExtension, - child: Consumer( - builder: (context, value, child) { - if (value.isToggled) { - return BlocProvider( - create: (context) => - DatabaseViewSettingExtensionBloc(viewId: viewId), - child: _DatabaseViewSettingContent( - fieldController: databaseController.fieldController, - ), - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - ); - } -} - -class _DatabaseViewSettingContent extends StatelessWidget { - const _DatabaseViewSettingContent({required this.fieldController}); - - final FieldController fieldController; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return 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), - ), - ], - ), - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 71b9fddda5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.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/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: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/style_widget/extension.dart'; -import 'package:flutter/material.dart'; - -class AddDatabaseViewButton extends StatefulWidget { - const AddDatabaseViewButton({super.key, required this.onTap}); - - final Function(DatabaseLayoutPB) onTap; - - @override - State createState() => _AddDatabaseViewButtonState(); -} - -class _AddDatabaseViewButtonState extends State { - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - constraints: BoxConstraints.loose(const Size(200, 400)), - direction: PopoverDirection.bottomWithLeftAligned, - 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, - ), - ), - popupBuilder: (BuildContext context) { - return TabBarAddButtonAction( - onTap: (action) { - popoverController.close(); - widget.onTap(action); - }, - ); - }, - ); - } -} - -class TabBarAddButtonAction extends StatelessWidget { - const TabBarAddButtonAction({super.key, required this.onTap}); - - final Function(DatabaseLayoutPB) onTap; - - @override - Widget build(BuildContext context) { - final cells = DatabaseLayoutPB.values.map((layout) { - return TabBarAddButtonActionCell( - action: layout, - onTap: onTap, - ); - }).toList(); - - return ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - itemBuilder: (BuildContext context, int index) => cells[index], - separatorBuilder: (BuildContext context, int index) => - VSpace(GridSize.typeOptionSeparatorHeight), - padding: const EdgeInsets.symmetric(vertical: 4.0), - ); - } -} - -class TabBarAddButtonActionCell extends StatelessWidget { - const TabBarAddButtonActionCell({ - super.key, - required this.action, - required this.onTap, - }); - - final DatabaseLayoutPB action; - final void Function(DatabaseLayoutPB) onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - '${LocaleKeys.grid_createView.tr()} ${action.layoutName}', - color: AFThemeExtension.of(context).textColor, - ), - leftIcon: FlowySvg( - action.icon, - color: Theme.of(context).iconTheme.color, - ), - onTap: () => onTap(action), - ).padding(horizontal: 6.0), - ); - } -} 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 deleted file mode 100644 index fa5e44a5e6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ /dev/null @@ -1,391 +0,0 @@ -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/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: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, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 35, - child: Stack( - children: [ - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Divider( - color: AFThemeExtension.of(context).borderColor, - height: 1, - thickness: 1, - ), - ), - 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), - ); - }, - ), - ), - ], - ), - ], - ), - ); - } - - Widget pageSettingBarFromState( - BuildContext context, - 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 tabBar.builder.settingBar(context, controller); - } -} - -class DatabaseTabBar extends StatefulWidget { - const DatabaseTabBar({super.key}); - - @override - State createState() => _DatabaseTabBarState(); -} - -class _DatabaseTabBarState extends State { - final _scrollController = ScrollController(); - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - @override - 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, - ), - ); - }, - ); - } -} - -class DatabaseTabBarItem extends StatelessWidget { - const DatabaseTabBarItem({ - super.key, - required this.view, - required this.isSelected, - required this.onTap, - }); - - final ViewPB view; - final bool isSelected; - final Function(ViewPB) onTap; - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - 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), - ), - ), - ), - if (isSelected) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Divider( - height: 2, - thickness: 2, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - ); - } -} - -class TabBarItemButton extends StatefulWidget { - const TabBarItemButton({ - super.key, - required this.view, - required this.isSelected, - required this.onTap, - }); - - final ViewPB view; - 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, - ), - 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(); - }, - ), - ], - ), - ), - ); - }, - 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, - ), - ), - ), - ); - } - - 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; - } -} - -enum TabBarViewAction implements ActionCell { - rename, - changeIcon, - delete; - - @override - String get name { - 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(); - } - } - - Widget icon(Color iconColor) { - 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); - } - } - - @override - Widget? leftIcon(Color iconColor) => icon(iconColor); - - @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 deleted file mode 100644 index 3a1fcac510..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart +++ /dev/null @@ -1,157 +0,0 @@ -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/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 { - const MobileTabBarHeader({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - left: GridSize.horizontalHeaderPadding, - top: 14.0, - right: GridSize.horizontalHeaderPadding - 5.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const _DatabaseViewSelectorButton(), - const Spacer(), - BlocBuilder( - builder: (context, state) { - final currentView = state.tabBars.firstWhereIndexedOrNull( - (index, tabBar) => index == state.selectedIndex, - ); - - if (currentView == null) { - return const SizedBox.shrink(); - } - - return MobileDatabaseControls( - controller: state - .tabBarControllerByViewId[currentView.viewId]!.controller, - features: switch (currentView.layout) { - ViewLayoutPB.Board || ViewLayoutPB.Calendar => [ - MobileDatabaseControlFeatures.filter, - ], - ViewLayoutPB.Grid => [ - MobileDatabaseControlFeatures.sort, - MobileDatabaseControlFeatures.filter, - ], - _ => [], - }, - ); - }, - ), - ], - ), - ); - } -} - -class _DatabaseViewSelectorButton extends StatelessWidget { - const _DatabaseViewSelectorButton(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final tabBar = state.tabBars.firstWhereIndexedOrNull( - (index, tabBar) => index == state.selectedIndex, - ); - - if (tabBar == null) { - return const SizedBox.shrink(); - } - - return TextButton( - style: ButtonStyle( - padding: const WidgetStatePropertyAll( - EdgeInsets.fromLTRB(12, 8, 8, 8), - ), - maximumSize: const WidgetStatePropertyAll(Size(200, 48)), - minimumSize: const WidgetStatePropertyAll(Size(48, 0)), - shape: const WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - backgroundColor: WidgetStatePropertyAll( - Theme.of(context).brightness == Brightness.light - ? const Color(0x0F212729) - : const Color(0x0FFFFFFF), - ), - overlayColor: WidgetStatePropertyAll( - Theme.of(context).colorScheme.secondary, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildViewIconButton(context, tabBar.view), - const HSpace(6), - Flexible( - child: FlowyText.medium( - tabBar.view.nameOrDefault, - fontSize: 14, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(8), - const FlowySvg( - FlowySvgs.arrow_tight_s, - size: Size.square(10), - ), - ], - ), - onPressed: () { - showTransitionMobileBottomSheet( - context, - showDivider: false, - builder: (_) { - return MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: context.read(), - ), - ], - child: const MobileDatabaseViewList(), - ); - }, - ); - }, - ); - }, - ); - } - - 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, - ); - } -} 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 deleted file mode 100644 index 7c2dc40869..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ /dev/null @@ -1,459 +0,0 @@ -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/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'; - -abstract class DatabaseTabBarItemBuilder { - const DatabaseTabBarItemBuilder(); - - /// Returns the content of the tab bar item. The content is shown when the tab - /// bar item is selected. It can be any kind of database view. - Widget content( - BuildContext context, - ViewPB view, - DatabaseController controller, - bool shrinkWrap, - String? initialRowId, - ); - - /// Returns the setting bar of the tab bar item. The setting bar is shown on the - /// top right conner when the tab bar item is selected. - Widget settingBar( - BuildContext context, - DatabaseController controller, - ); - - Widget settingBarExtension( - BuildContext context, - DatabaseController controller, - ); - - /// Should be called in case a builder has resources it - /// needs to dispose of. - /// - // If we add any logic in this method, add @mustCallSuper ! - void dispose() {} -} - -class DatabaseTabBarView extends StatefulWidget { - const DatabaseTabBarView({ - 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 - /// - final String? initialRowId; - - @override - State createState() => _DatabaseTabBarViewState(); -} - -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); - }); - } - } - - @override - void 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()), - ), - BlocProvider( - create: (_) => 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(); - } - - 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 child; - }, - ), - ); - } - - Future fetchLocalCompactMode(String compactModeId) 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('fetch local compact mode from id :$compactModeId failed', e); - } - return compactModeIds.contains(compactModeId); - } - - 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, - ) { - 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, - ), - ); - } -} - -class DatabaseTabBarViewPlugin extends Plugin { - DatabaseTabBarViewPlugin({ - required ViewPB view, - required PluginType pluginType, - this.initialRowId, - }) : _pluginType = pluginType, - notifier = ViewPluginNotifier(view: view); - - @override - final ViewPluginNotifier notifier; - - final PluginType _pluginType; - late final ViewInfoBloc _viewInfoBloc; - - /// Used to open a Row on plugin load - /// - final String? initialRowId; - - @override - PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder( - bloc: _viewInfoBloc, - notifier: notifier, - initialRowId: initialRowId, - ); - - @override - PluginId get id => notifier.view.id; - - @override - PluginType get pluginType => _pluginType; - - @override - void init() { - _viewInfoBloc = ViewInfoBloc(view: notifier.view) - ..add(const ViewInfoEvent.started()); - } - - @override - void dispose() { - _viewInfoBloc.close(); - notifier.dispose(); - } -} - -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, - required this.notifier, - this.initialRowId, - }); - - final ViewInfoBloc bloc; - final ViewPluginNotifier notifier; - - /// Used to open a Row on plugin load - /// - 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); - - @override - Widget buildWidget({ - required PluginContext context, - required bool shrinkWrap, - Map? data, - }) { - notifier.isDeleted.addListener(() { - final deletedView = notifier.isDeleted.value; - if (deletedView != null && deletedView.hasIndex()) { - context.onDeleted?.call(notifier.view, deletedView.index); - } - }); - - 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, - ), - ); - } - - @override - List get navigationItems => [this]; - - @override - Widget? get rightBarItem { - final view = notifier.view; - return BlocProvider.value( - value: bloc, - child: Row( - children: [ - ShareButton(key: ValueKey(view.id), view: view), - const HSpace(10), - ViewFavoriteButton(view: view), - const HSpace(4), - MoreViewActions(view: view), - ], - ), - ); - } - - @override - EdgeInsets get contentPadding => const EdgeInsets.only(top: 28); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart deleted file mode 100644 index 68c4b15d5c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart +++ /dev/null @@ -1,471 +0,0 @@ -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_editor/appflowy_editor.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 '../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'; - -/// Edit a database row with card style widget -class RowCard extends StatefulWidget { - const RowCard({ - super.key, - required this.fieldController, - required this.rowMeta, - required this.viewId, - required this.isEditing, - required this.rowCache, - required this.cellBuilder, - required this.onTap, - required this.onStartEditing, - required this.onEndEditing, - required this.styleConfiguration, - this.onShiftTap, - this.groupingFieldId, - this.groupId, - required this.userProfile, - this.isCompact = false, - }); - - final FieldController fieldController; - final RowMetaPB rowMeta; - final String viewId; - final String? groupingFieldId; - final String? groupId; - - final bool isEditing; - final RowCache rowCache; - - /// The [CardCellBuilder] is used to build the card cells. - final CardCellBuilder cellBuilder; - - /// Called when the user taps on the card. - final void Function(BuildContext context) onTap; - - final void Function(BuildContext context)? onShiftTap; - - /// Called when the user starts editing the card. - final VoidCallback onStartEditing; - - /// Called when the user ends editing the card. - final VoidCallback onEndEditing; - - 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(); -} - -class _RowCardState extends State { - final popoverController = PopoverController(); - late final CardBloc _cardBloc; - - @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, - )..add(const CardEvent.initial()); - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (widget.isEditing != _cardBloc.state.isEditing) { - _cardBloc.add(CardEvent.setIsEditing(widget.isEditing)); - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - _cardBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _cardBloc, - child: BlocListener( - listenWhen: (previous, current) => - previous.isEditing != current.isEditing, - listener: (context, state) { - if (!state.isEditing) { - widget.onEndEditing(); - } - }, - child: UniversalPlatform.isMobile ? _mobile() : _desktop(), - ), - ); - } - - 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 _desktop() { - final accessories = widget.styleConfiguration.showAccessory - ? const [ - EditCardAccessory(), - MoreCardOptionsAccessory(), - ] - : null; - return AppFlowyPopover( - controller: popoverController, - 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, - ); - }, - ), - ); - }, - ), - ); - } - - void _handleOpenAccessory(AccessoryType newAccessoryType) { - switch (newAccessoryType) { - case AccessoryType.edit: - widget.onStartEditing(); - break; - case AccessoryType.more: - popoverController.show(); - break; - } - } -} - -class _CardContent extends StatelessWidget { - const _CardContent({ - required this.rowMeta, - 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), - ), - ), - ], - ); - return styleConfiguration.hoverStyle == null - ? child - : FlowyHover( - style: styleConfiguration.hoverStyle, - buildWhenOnHover: () => !context.read().state.isEditing, - child: child, - ); - } - - List _makeCells( - BuildContext context, - 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(); - } -} - -class _CardContentCell extends StatefulWidget { - const _CardContentCell({ - required this.cellBuilder, - required this.cellMeta, - required this.rowMeta, - required this.isTitle, - required this.styleMap, - }); - - 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, - 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, - ), - ); - } - - 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(); - } -} - -class EditCardAccessory extends StatelessWidget with CardAccessory { - const EditCardAccessory({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(3.0), - child: FlowySvg( - FlowySvgs.edit_s, - color: Theme.of(context).hintColor, - ), - ); - } - - @override - AccessoryType get type => AccessoryType.edit; -} - -class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory { - const MoreCardOptionsAccessory({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(3.0), - child: FlowySvg( - FlowySvgs.three_dots_s, - color: Theme.of(context).hintColor, - ), - ); - } - - @override - AccessoryType get type => AccessoryType.more; -} - -class RowCardStyleConfiguration { - const RowCardStyleConfiguration({ - required this.cellStyleMap, - this.showAccessory = true, - this.cardPadding = const EdgeInsets.all(4), - this.hoverStyle, - }); - - final CardCellStyleMap cellStyleMap; - final bool showAccessory; - final EdgeInsets cardPadding; - final HoverStyle? 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 deleted file mode 100644 index 04f9bb652c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart +++ /dev/null @@ -1,170 +0,0 @@ -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'; - -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/widgets/setting/field_visibility_extension.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'card_bloc.freezed.dart'; - -class CardBloc extends Bloc { - CardBloc({ - required this.fieldController, - required this.groupFieldId, - required this.viewId, - required bool isEditing, - required this.rowController, - }) : super( - CardState.initial( - _makeCells( - fieldController, - groupFieldId, - rowController, - ), - isEditing, - rowController.rowMeta, - ), - ) { - rowController.initialize(); - _dispatch(); - } - - final FieldController fieldController; - final String? groupFieldId; - final String viewId; - final RowController rowController; - - VoidCallback? _rowCallback; - - @override - Future close() async { - if (_rowCallback != null) { - _rowCallback = null; - } - await rowController.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - await _startListening(); - }, - didReceiveCells: (cells, reason) async { - emit( - state.copyWith( - cells: cells, - changeReason: reason, - ), - ); - }, - setIsEditing: (bool isEditing) { - if (isEditing != state.isEditing) { - emit(state.copyWith(isEditing: isEditing)); - } - }, - didUpdateRowMeta: (rowMeta) { - emit(state.copyWith(rowMeta: rowMeta)); - }, - ); - }, - ); - } - - Future _startListening() async { - rowController.addListener( - onRowChanged: (cellMap, reason) { - if (!isClosed) { - final cells = - _makeCells(fieldController, groupFieldId, rowController); - add(CardEvent.didReceiveCells(cells, reason)); - } - }, - onMetaChanged: () { - if (!isClosed) { - add(CardEvent.didUpdateRowMeta(rowController.rowMeta)); - } - }, - ); - } -} - -List _makeCells( - FieldController fieldController, - String? groupFieldId, - RowController rowController, -) { - // Only show the non-hidden cells and cells that aren't of the grouping field - final cellContext = rowController.loadCells(); - - cellContext.removeWhere((cellContext) { - final fieldInfo = fieldController.getField(cellContext.fieldId); - return fieldInfo == null || - !(fieldInfo.visibility?.isVisibleState() ?? false) || - (groupFieldId != null && cellContext.fieldId == groupFieldId); - }); - return cellContext - .map( - (cellCtx) => CellMeta( - fieldId: cellCtx.fieldId, - rowId: cellCtx.rowId, - fieldType: fieldController.getField(cellCtx.fieldId)!.fieldType, - ), - ) - .toList(); -} - -@freezed -class CardEvent with _$CardEvent { - const factory CardEvent.initial() = _InitialRow; - const factory CardEvent.setIsEditing(bool isEditing) = _IsEditing; - const factory CardEvent.didReceiveCells( - List cells, - ChangedReason reason, - ) = _DidReceiveCells; - const factory CardEvent.didUpdateRowMeta(RowMetaPB rowMeta) = - _DidUpdateRowMeta; -} - -@freezed -class CellMeta with _$CellMeta { - const CellMeta._(); - - const factory CellMeta({ - required String fieldId, - required RowId rowId, - required FieldType fieldType, - }) = _DatabaseCellMeta; - - CellContext cellContext() => CellContext(fieldId: fieldId, rowId: rowId); -} - -@freezed -class CardState with _$CardState { - const factory CardState({ - required List cells, - required bool isEditing, - required RowMetaPB rowMeta, - ChangedReason? changeReason, - }) = _RowCardState; - - factory CardState.initial( - List cells, - bool isEditing, - RowMetaPB rowMeta, - ) => - CardState( - cells: cells, - isEditing: isEditing, - rowMeta: rowMeta, - ); -} 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 deleted file mode 100644 index e74f947b46..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -enum AccessoryType { - edit, - more, -} - -abstract mixin class CardAccessory implements Widget { - AccessoryType get type; - void onTap(BuildContext context) {} -} - -class CardAccessoryContainer extends StatelessWidget { - const CardAccessoryContainer({ - super.key, - required this.accessories, - required this.onTapAccessory, - }); - - final List accessories; - final void Function(AccessoryType) onTapAccessory; - - @override - Widget build(BuildContext context) { - if (accessories.isEmpty) { - return const SizedBox.shrink(); - } - - final children = accessories.map((accessory) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - accessory.onTap(context); - onTapAccessory(accessory.type); - }, - child: _wrapHover(context, accessory), - ); - }).toList(); - - children.insert( - 1, - VerticalDivider( - width: 1, - thickness: 1, - color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withValues(alpha: 0.12) - : const Color(0xff59647a), - ), - ); - - return _wrapDecoration( - context, - IntrinsicHeight(child: Row(children: children)), - ); - } - - Widget _wrapHover(BuildContext context, CardAccessory accessory) { - return SizedBox( - width: 24, - height: 22, - child: FlowyHover( - style: HoverStyle( - backgroundColor: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.zero, - ), - child: accessory, - ), - ); - } - - Widget _wrapDecoration(BuildContext context, Widget child) { - final decoration = BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(4)), - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withValues(alpha: 0.12) - : const Color(0xff59647a), - ), - ), - boxShadow: [ - BoxShadow( - blurRadius: 4, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - ), - BoxShadow( - blurRadius: 4, - spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), - ), - ], - ); - return Container( - clipBehavior: Clip.hardEdge, - decoration: decoration, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: child, - ), - ); - } -} 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 deleted file mode 100644 index a91ffae42d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -import 'accessory.dart'; - -class RowCardContainer extends StatelessWidget { - const RowCardContainer({ - super.key, - required this.child, - required this.onTap, - required this.openAccessory, - required this.accessories, - this.buildAccessoryWhen, - this.onShiftTap, - }); - - final Widget child; - final void Function(BuildContext) onTap; - final void Function(BuildContext)? onShiftTap; - final void Function(AccessoryType) openAccessory; - final List accessories; - final bool Function()? buildAccessoryWhen; - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => _CardContainerNotifier(), - child: Consumer<_CardContainerNotifier>( - builder: (context, notifier, _) { - final shouldBuildAccessory = buildAccessoryWhen?.call() ?? true; - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (HardwareKeyboard.instance.isShiftPressed) { - onShiftTap?.call(context); - } else { - onTap(context); - } - }, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 36), - child: _CardEnterRegion( - shouldBuildAccessory: shouldBuildAccessory, - accessories: accessories, - onTapAccessory: openAccessory, - child: child, - ), - ), - ); - }, - ), - ); - } -} - -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; - - @override - Widget build(BuildContext context) { - return Selector<_CardContainerNotifier, bool>( - selector: (context, notifier) => notifier.onEnter, - builder: (context, onEnter, _) { - final List children = [ - child, - if (onEnter && shouldBuildAccessory) - Positioned( - top: 7.0, - right: 7.0, - child: CardAccessoryContainer( - accessories: accessories, - onTapAccessory: onTapAccessory, - ), - ), - ]; - - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (p) => - Provider.of<_CardContainerNotifier>(context, listen: false) - .onEnter = true, - onExit: (p) => - Provider.of<_CardContainerNotifier>(context, listen: false) - .onEnter = false, - child: IntrinsicHeight( - child: Stack( - alignment: AlignmentDirectional.topEnd, - fit: StackFit.expand, - children: children, - ), - ), - ); - }, - ); - } -} - -class _CardContainerNotifier extends ChangeNotifier { - _CardContainerNotifier(); - - bool _onEnter = false; - - set onEnter(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get onEnter => _onEnter; -} 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 deleted file mode 100644 index d17c522de6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart +++ /dev/null @@ -1,126 +0,0 @@ -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_backend/protobuf/flowy-database2/field_entities.pb.dart'; - -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'; -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'; - -typedef CardCellStyleMap = Map; - -class CardCellBuilder { - CardCellBuilder({required this.databaseController}); - - final DatabaseController databaseController; - - Widget build({ - required CellContext cellContext, - required CardCellStyleMap styleMap, - EditableCardNotifier? cellNotifier, - required bool hasNotes, - }) { - final fieldType = databaseController.fieldController - .getField(cellContext.fieldId)! - .fieldType; - final key = ValueKey( - "${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}", - ); - final style = styleMap[fieldType]; - return switch (fieldType) { - FieldType.Checkbox => CheckboxCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.Checklist => ChecklistCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.DateTime => DateCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.LastEditedTime || FieldType.CreatedTime => TimestampCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.SingleSelect || FieldType.MultiSelect => SelectOptionCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.Number => NumberCardCell( - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - key: key, - ), - FieldType.RichText => TextCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - editableNotifier: cellNotifier, - showNotes: hasNotes, - ), - FieldType.URL => URLCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.Relation => RelationCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.Summary => SummaryCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.Time => TimeCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), - FieldType.Translate => TranslateCardCell( - key: key, - style: isStyleOrNull(style), - 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/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/card_cell.dart deleted file mode 100644 index 7b03539252..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/card_cell.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; - -abstract class CardCell extends StatefulWidget { - const CardCell({super.key, required this.style}); - - final T style; -} - -abstract class CardCellStyle { - const CardCellStyle({required this.padding}); - - final EdgeInsetsGeometry padding; -} - -S? isStyleOrNull(CardCellStyle? style) { - if (style is S) { - return style as S; - } else { - return null; - } -} - -class EditableCardNotifier { - EditableCardNotifier({bool isEditing = false}) - : isCellEditing = ValueNotifier(isEditing); - - final ValueNotifier isCellEditing; - - void dispose() { - isCellEditing.dispose(); - } -} - -abstract mixin class EditableCell { - // Each cell notifier will be bind to the [EditableRowNotifier], which enable - // the row notifier receive its cells event. For example: begin editing the - // cell or end editing the cell. - // - EditableCardNotifier? get editableNotifier; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checkbox_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checkbox_card_cell.dart deleted file mode 100644 index 0d1f4687fe..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checkbox_card_cell.dart +++ /dev/null @@ -1,87 +0,0 @@ -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/checkbox_cell_bloc.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class CheckboxCardCellStyle extends CardCellStyle { - CheckboxCardCellStyle({ - required super.padding, - required this.iconSize, - required this.showFieldName, - this.textStyle, - }) : assert(!showFieldName || showFieldName && textStyle != null); - - final Size iconSize; - final bool showFieldName; - final TextStyle? textStyle; -} - -class CheckboxCardCell extends CardCell { - const CheckboxCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _CheckboxCellState(); -} - -class _CheckboxCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) { - return CheckboxCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - )..add(const CheckboxCellEvent.initial()); - }, - child: BlocBuilder( - builder: (context, state) { - return Container( - alignment: AlignmentDirectional.centerStart, - padding: widget.style.padding, - child: Row( - children: [ - FlowyIconButton( - icon: FlowySvg( - state.isSelected - ? FlowySvgs.check_filled_s - : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - size: widget.style.iconSize, - ), - width: 20, - onPressed: () => context - .read() - .add(const CheckboxCellEvent.select()), - ), - if (widget.style.showFieldName) ...[ - const HSpace(6.0), - Text( - state.fieldName, - style: widget.style.textStyle, - ), - ], - ], - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart deleted file mode 100644 index a45b621dde..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart +++ /dev/null @@ -1,64 +0,0 @@ -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/checklist_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class ChecklistCardCellStyle extends CardCellStyle { - ChecklistCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class ChecklistCardCell extends CardCell { - const ChecklistCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _ChecklistCellState(); -} - -class _ChecklistCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) { - return ChecklistCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - builder: (context, state) { - if (state.tasks.isEmpty) { - return const SizedBox.shrink(); - } - return Padding( - padding: widget.style.padding, - child: ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - textStyle: widget.style.textStyle, - ), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index c459d8cc60..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/date_card_cell.dart +++ /dev/null @@ -1,83 +0,0 @@ -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 { - DateCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class DateCardCell extends CardCell { - const DateCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _DateCellState(); -} - -class _DateCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return DateCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - builder: (context, state) { - final dateStr = getDateCellStrFromCellData( - state.fieldInfo, - state.cellData, - ); - - if (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), - ], - ], - ), - ); - }, - ), - ); - } -} 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/number_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/number_card_cell.dart deleted file mode 100644 index ee160f09be..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/number_card_cell.dart +++ /dev/null @@ -1,62 +0,0 @@ -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/number_cell_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class NumberCardCellStyle extends CardCellStyle { - const NumberCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class NumberCardCell extends CardCell { - const NumberCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _NumberCellState(); -} - -class _NumberCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return NumberCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - buildWhen: (previous, current) => previous.content != current.content, - builder: (context, state) { - if (state.content.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - alignment: AlignmentDirectional.centerStart, - padding: widget.style.padding, - child: Text(state.content, style: widget.style.textStyle), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart deleted file mode 100644 index 023048355e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:appflowy/generated/locale_keys.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/relation_cell_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class RelationCardCellStyle extends CardCellStyle { - RelationCardCellStyle({ - required super.padding, - required this.textStyle, - required this.wrap, - }); - - final TextStyle textStyle; - final bool wrap; -} - -class RelationCardCell extends CardCell { - const RelationCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _RelationCellState(); -} - -class _RelationCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) { - return RelationCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - builder: (context, state) { - if (state.rows.isEmpty) { - return const SizedBox.shrink(); - } - - final children = state.rows.map( - (row) { - final isEmpty = row.name.isEmpty; - return Text( - isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, - style: widget.style.textStyle.copyWith( - color: isEmpty ? Theme.of(context).hintColor : null, - decoration: TextDecoration.underline, - ), - overflow: TextOverflow.ellipsis, - ); - }, - ).toList(); - - return Container( - alignment: AlignmentDirectional.topStart, - padding: widget.style.padding, - child: widget.style.wrap - ? Wrap(spacing: 4, runSpacing: 4, children: children) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/select_option_card_cell.dart deleted file mode 100644 index b705e93abb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/select_option_card_cell.dart +++ /dev/null @@ -1,87 +0,0 @@ -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_editor/extension.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class SelectOptionCardCellStyle extends CardCellStyle { - SelectOptionCardCellStyle({ - required super.padding, - required this.tagFontSize, - required this.wrap, - required this.tagPadding, - }); - - final double tagFontSize; - final bool wrap; - final EdgeInsets tagPadding; -} - -class SelectOptionCardCell extends CardCell { - const SelectOptionCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _SelectOptionCellState(); -} - -class _SelectOptionCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) { - return SelectOptionCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - buildWhen: (previous, current) { - return previous.selectedOptions != current.selectedOptions; - }, - builder: (context, state) { - if (state.selectedOptions.isEmpty) { - return const SizedBox.shrink(); - } - - final children = state.selectedOptions - .map( - (option) => SelectOptionTag( - option: option, - fontSize: widget.style.tagFontSize, - padding: widget.style.tagPadding, - ), - ) - .toList(); - - return Container( - alignment: AlignmentDirectional.topStart, - padding: widget.style.padding, - child: widget.style.wrap - ? Wrap(spacing: 4, runSpacing: 4, children: children) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart deleted file mode 100644 index 8d7577b6ea..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/summary_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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class SummaryCardCellStyle extends CardCellStyle { - const SummaryCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class SummaryCardCell extends CardCell { - const SummaryCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _SummaryCellState(); -} - -class _SummaryCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return SummaryCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - buildWhen: (previous, current) => previous.content != current.content, - builder: (context, state) { - if (state.content.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - alignment: AlignmentDirectional.centerStart, - padding: widget.style.padding, - child: Text(state.content, style: widget.style.textStyle), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index a3758029d1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart +++ /dev/null @@ -1,276 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_builder.dart'; -import 'card_cell.dart'; - -class TextCardCellStyle extends CardCellStyle { - TextCardCellStyle({ - required super.padding, - required this.textStyle, - required this.titleTextStyle, - this.maxLines = 1, - }); - - final TextStyle textStyle; - final TextStyle titleTextStyle; - final int? maxLines; -} - -class TextCardCell extends CardCell with EditableCell { - const TextCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - this.showNotes = false, - this.editableNotifier, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final bool showNotes; - - @override - final EditableCardNotifier? editableNotifier; - - @override - State createState() => _TextCellState(); -} - -class _TextCellState extends State { - late final cellBloc = TextCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - late final TextEditingController _textEditingController; - final focusNode = SingleListenerFocusNode(); - - @override - void initState() { - super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); - - if (widget.editableNotifier?.isCellEditing.value ?? false) { - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - cellBloc.add(const TextCellEvent.enableEdit(true)); - }); - } - - // 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); - _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) { - return; - } - - final isEditing = widget.editableNotifier?.isCellEditing.value ?? false; - if (isEditing) { - WidgetsBinding.instance - .addPostFrameCallback((_) => focusNode.requestFocus()); - } - cellBloc.add(TextCellEvent.enableEdit(isEditing)); - }); - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (oldWidget.editableNotifier != widget.editableNotifier) { - _bindEditableNotifier(); - } - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - final isTitle = cellBloc.cellController.fieldInfo.isPrimary; - return BlocProvider.value( - value: cellBloc, - child: BlocListener( - listenWhen: (previous, current) => previous.content != current.content, - listener: (context, state) { - _textEditingController.text = state.content ?? ""; - }, - child: isTitle ? _buildTitle() : _buildText(), - ), - ); - } - - @override - void dispose() { - _textEditingController.dispose(); - widget.editableNotifier?.isCellEditing - .removeListener(_bindEditableNotifier); - focusNode.dispose(); - cellBloc.close(); - super.dispose(); - } - - Widget? _buildIcon(TextCellState state) { - if (state.emoji?.value.isNotEmpty ?? false) { - return FlowyText.emoji( - optimizeEmojiAlign: true, - state.emoji?.value ?? '', - ); - } - - 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, - ), - ), - ); - } - return null; - } - - Widget _buildText() { - return BlocBuilder( - builder: (context, state) { - final content = state.content ?? ""; - - return content.isEmpty - ? const SizedBox.shrink() - : Container( - padding: widget.style.padding, - alignment: AlignmentDirectional.centerStart, - child: Text( - content, - style: widget.style.textStyle, - maxLines: widget.style.maxLines, - ), - ); - }, - ); - } - - Widget _buildTitle() { - final textField = _buildTextField(); - 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, - ), - Expanded(child: textField), - ], - ); - }, - ); - } - - Widget _buildTextField() { - return BlocSelector( - selector: (state) => state.enableEdit, - builder: (context, isEditing) { - return IgnorePointer( - ignoring: !isEditing, - child: CallbackShortcuts( - 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, - minLines: 1, - textInputAction: TextInputAction.done, - readOnly: !isEditing, - enableInteractiveSelection: isEditing, - style: widget.style.titleTextStyle, - decoration: InputDecoration( - contentPadding: widget.style.padding, - border: InputBorder.none, - enabledBorder: InputBorder.none, - isDense: true, - isCollapsed: true, - hintText: LocaleKeys.grid_row_titlePlaceholder.tr(), - hintStyle: widget.style.titleTextStyle.copyWith( - color: Theme.of(context).hintColor, - ), - ), - onTapOutside: (_) {}, - ), - ), - ); - }, - ); - } -} - -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_skeleton/time_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/time_card_cell.dart deleted file mode 100644 index 68a95e53e2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/time_card_cell.dart +++ /dev/null @@ -1,62 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; - -import 'card_cell.dart'; - -class TimeCardCellStyle extends CardCellStyle { - const TimeCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class TimeCardCell extends CardCell { - const TimeCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _TimeCellState(); -} - -class _TimeCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return TimeCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - buildWhen: (previous, current) => previous.content != current.content, - builder: (context, state) { - if (state.content.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - alignment: AlignmentDirectional.centerStart, - padding: widget.style.padding, - child: Text(state.content, style: widget.style.textStyle), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart deleted file mode 100644 index 5e560bbee4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart +++ /dev/null @@ -1,65 +0,0 @@ -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/timestamp_cell_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class TimestampCardCellStyle extends CardCellStyle { - TimestampCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class TimestampCardCell extends CardCell { - const TimestampCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _TimestampCellState(); -} - -class _TimestampCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) { - return TimestampCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - buildWhen: (previous, current) => previous.dateStr != current.dateStr, - builder: (context, state) { - if (state.dateStr.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - alignment: AlignmentDirectional.centerStart, - padding: widget.style.padding, - child: Text( - state.dateStr, - style: widget.style.textStyle, - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart deleted file mode 100644 index e9c233af18..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/translate_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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class TranslateCardCellStyle extends CardCellStyle { - const TranslateCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class TranslateCardCell extends CardCell { - const TranslateCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _TranslateCellState(); -} - -class _TranslateCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return TranslateCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - buildWhen: (previous, current) => previous.content != current.content, - builder: (context, state) { - if (state.content.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - alignment: AlignmentDirectional.centerStart, - padding: widget.style.padding, - child: Text(state.content, style: widget.style.textStyle), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/url_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/url_card_cell.dart deleted file mode 100644 index 92b210d184..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/url_card_cell.dart +++ /dev/null @@ -1,64 +0,0 @@ -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/url_cell_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'card_cell.dart'; - -class URLCardCellStyle extends CardCellStyle { - URLCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class URLCardCell extends CardCell { - const URLCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _URLCellState(); -} - -class _URLCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return URLCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - }, - child: BlocBuilder( - buildWhen: (previous, current) => previous.content != current.content, - builder: (context, state) { - if (state.content.isEmpty) { - return const SizedBox.shrink(); - } - return Container( - alignment: AlignmentDirectional.centerStart, - padding: widget.style.padding, - child: Text( - state.content, - style: widget.style.textStyle, - ), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 23a8a2451f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.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) { - const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 2); - final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 10, - overflow: TextOverflow.ellipsis, - fontWeight: FontWeight.w400, - ); - - return { - FieldType.Checkbox: CheckboxCardCellStyle( - padding: padding, - iconSize: const Size.square(16), - showFieldName: true, - textStyle: textStyle, - ), - FieldType.Checklist: ChecklistCardCellStyle( - padding: padding, - textStyle: textStyle.copyWith(color: Theme.of(context).hintColor), - ), - FieldType.CreatedTime: TimestampCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.DateTime: DateCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.LastEditedTime: TimestampCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.MultiSelect: SelectOptionCardCellStyle( - padding: padding, - tagFontSize: 9, - wrap: true, - tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - ), - FieldType.Number: NumberCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.RichText: TextCardCellStyle( - padding: padding, - textStyle: textStyle, - titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 11, - overflow: TextOverflow.ellipsis, - ), - ), - FieldType.SingleSelect: SelectOptionCardCellStyle( - padding: padding, - tagFontSize: 9, - wrap: true, - tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - ), - FieldType.URL: URLCardCellStyle( - padding: padding, - textStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - ), - FieldType.Relation: RelationCardCellStyle( - padding: padding, - wrap: true, - textStyle: textStyle, - ), - FieldType.Summary: SummaryCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.Translate: TranslateCardCellStyle( - 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 deleted file mode 100644 index 6264fea958..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter/material.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) { - const EdgeInsetsGeometry padding = EdgeInsets.all(4); - final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 11, - overflow: TextOverflow.ellipsis, - ); - - return { - FieldType.Checkbox: CheckboxCardCellStyle( - padding: padding, - iconSize: const Size.square(16), - showFieldName: true, - textStyle: textStyle, - ), - FieldType.Checklist: ChecklistCardCellStyle( - padding: padding, - textStyle: textStyle.copyWith(color: Theme.of(context).hintColor), - ), - FieldType.CreatedTime: TimestampCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.DateTime: DateCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.LastEditedTime: TimestampCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.MultiSelect: SelectOptionCardCellStyle( - padding: padding, - tagFontSize: 11, - wrap: true, - tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - ), - FieldType.Number: NumberCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.RichText: TextCardCellStyle( - padding: padding, - textStyle: textStyle, - maxLines: 2, - titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - overflow: TextOverflow.ellipsis, - ), - ), - FieldType.SingleSelect: SelectOptionCardCellStyle( - padding: padding, - tagFontSize: 11, - wrap: true, - tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - ), - FieldType.URL: URLCardCellStyle( - padding: padding, - textStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - ), - FieldType.Relation: RelationCardCellStyle( - padding: padding, - wrap: true, - textStyle: textStyle, - ), - FieldType.Summary: SummaryCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.Time: TimeCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.Translate: TranslateCardCellStyle( - 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 deleted file mode 100644 index 93d98f013e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart +++ /dev/null @@ -1,103 +0,0 @@ -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'; - -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/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/text_card_cell.dart'; -import '../card_cell_skeleton/time_card_cell.dart'; -import '../card_cell_skeleton/timestamp_card_cell.dart'; -import '../card_cell_skeleton/url_card_cell.dart'; - -CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) { - const EdgeInsetsGeometry padding = EdgeInsets.all(4); - final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 14, - overflow: TextOverflow.ellipsis, - fontWeight: FontWeight.w400, - ); - - return { - FieldType.Checkbox: CheckboxCardCellStyle( - padding: padding, - iconSize: const Size.square(24), - showFieldName: true, - textStyle: textStyle, - ), - FieldType.Checklist: ChecklistCardCellStyle( - padding: padding, - textStyle: textStyle.copyWith(color: Theme.of(context).hintColor), - ), - FieldType.CreatedTime: TimestampCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.DateTime: DateCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.LastEditedTime: TimestampCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.MultiSelect: SelectOptionCardCellStyle( - padding: padding, - tagFontSize: 12, - wrap: true, - tagPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - ), - FieldType.Number: NumberCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.RichText: TextCardCellStyle( - padding: padding, - textStyle: textStyle, - titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - overflow: TextOverflow.ellipsis, - ), - ), - FieldType.SingleSelect: SelectOptionCardCellStyle( - padding: padding, - tagFontSize: 12, - wrap: true, - tagPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - ), - FieldType.URL: URLCardCellStyle( - padding: padding, - textStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - ), - FieldType.Relation: RelationCardCellStyle( - padding: padding, - textStyle: textStyle, - wrap: true, - ), - FieldType.Summary: SummaryCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.Time: TimeCardCellStyle( - padding: padding, - textStyle: textStyle, - ), - FieldType.Translate: TranslateCardCellStyle( - 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 deleted file mode 100644 index 74abcecb3a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart +++ /dev/null @@ -1,42 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/checkbox.dart'; - -class DesktopGridCheckboxCellSkin extends IEditableCheckboxCellSkin { - @override - 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, - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index ebc4a6f976..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart +++ /dev/null @@ -1,65 +0,0 @@ -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: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'; - -class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - ChecklistCellBloc bloc, - PopoverController popoverController, - ) { - return AppFlowyPopover( - margin: EdgeInsets.zero, - controller: popoverController, - constraints: BoxConstraints.loose(const Size(360, 400)), - direction: PopoverDirection.bottomWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - skipTraversal: true, - popupBuilder: (popoverContext) { - WidgetsBinding.instance.addPostFrameCallback((_) { - cellContainerNotifier.isFocus = true; - }); - return BlocProvider.value( - value: bloc, - child: ChecklistCellEditor( - cellController: bloc.cellController, - ), - ); - }, - 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, - ), - ); - }, - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index de7f7f5a2e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart +++ /dev/null @@ -1,91 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -import '../editable_cell_skeleton/date.dart'; - -class DesktopGridDateCellSkin extends IEditableDateCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - DateCellBloc bloc, - DateCellState state, - PopoverController popoverController, - ) { - return AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(260, 620)), - margin: EdgeInsets.zero, - child: Align( - alignment: AlignmentDirectional.centerStart, - child: state.fieldInfo.wrapCellContent ?? false - ? _buildCellContent(state, compactModeNotifier) - : SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - child: _buildCellContent(state, compactModeNotifier), - ), - ), - popupBuilder: (BuildContext popoverContent) { - return DateCellEditor( - cellController: bloc.cellController, - onDismissed: () => cellContainerNotifier.isFocus = false, - ); - }, - onClose: () { - cellContainerNotifier.isFocus = false; - }, - ); - } - - Widget _buildCellContent( - DateCellState state, - ValueNotifier compactModeNotifier, - ) { - 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), - ), - ], - ], - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 7a6f3e63bc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart +++ /dev/null @@ -1,47 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_skeleton/number.dart'; - -class DesktopGridNumberCellSkin extends IEditableNumberCellSkin { - @override - 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, - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index dda3183b59..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart +++ /dev/null @@ -1,110 +0,0 @@ -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_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 '../editable_cell_skeleton/relation.dart'; - -class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { - @override - 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, - onClose: () => cellContainerNotifier.isFocus = false, - popupBuilder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: userWorkspaceBloc), - 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); - }, - ), - ), - ); - } - - Widget _buildWrapRows( - BuildContext context, - List rows, - bool compactMode, - ) { - return Padding( - padding: compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets, - child: Wrap( - runSpacing: 4, - spacing: 4.0, - children: rows.map( - (row) { - final isEmpty = row.name.isEmpty; - return FlowyText( - isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, - color: isEmpty ? Theme.of(context).hintColor : null, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ); - }, - ).toList(), - ), - ); - } - - Widget _buildNoWrapRows( - BuildContext context, - List rows, - bool compactMode, - ) { - return SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - child: Padding( - padding: GridSize.cellContentInsets, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4.0), - mainAxisSize: MainAxisSize.min, - children: rows.map( - (row) { - final isEmpty = row.name.isEmpty; - return FlowyText( - isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, - color: isEmpty ? Theme.of(context).hintColor : null, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ); - }, - ).toList(), - ), - ), - ); - } -} 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 deleted file mode 100644 index b599acc4f1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart +++ /dev/null @@ -1,128 +0,0 @@ -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_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_skeleton/select_option.dart'; - -class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SelectOptionCellBloc bloc, - PopoverController popoverController, - ) { - return AppFlowyPopover( - controller: popoverController, - constraints: const BoxConstraints.tightFor(width: 300), - margin: EdgeInsets.zero, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (BuildContext popoverContext) { - return SelectOptionCellEditor( - cellController: bloc.cellController, - ); - }, - onClose: () => cellContainerNotifier.isFocus = false, - child: BlocBuilder( - builder: (context, state) { - return Align( - alignment: AlignmentDirectional.centerStart, - child: state.wrap - ? _buildWrapOptions( - context, - state.selectedOptions, - compactModeNotifier, - ) - : _buildNoWrapOptions( - context, - state.selectedOptions, - compactModeNotifier, - ), - ); - }, - ), - ); - } - - 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 _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(), - ), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 1f3ded0109..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:styled_widget/styled_widget.dart'; - -class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SummaryCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return ChangeNotifierProvider( - create: (_) => SummaryMouseNotifier(), - builder: (context, child) { - 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: 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, - ), - ), - ), - 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), - ], - ), - ); - }, - ), - ); - }, - ); - } -} - -class SummaryMouseNotifier extends ChangeNotifier { - SummaryMouseNotifier(); - - bool _onEnter = false; - - set onEnter(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get onEnter => _onEnter; -} 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 deleted file mode 100644 index 75c973d886..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart +++ /dev/null @@ -1,110 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_skeleton/text.dart'; - -class DesktopGridTextCellSkin extends IEditableTextCellSkin { - @override - 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, - ), - ) - : 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(); - }, - ); - }, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart deleted file mode 100644 index a948e92b03..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart +++ /dev/null @@ -1,37 +0,0 @@ -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/time_cell_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_skeleton/time.dart'; - -class DesktopGridTimeCellSkin extends IEditableTimeCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - TimeCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - 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_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart deleted file mode 100644 index 8a1fd92499..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart +++ /dev/null @@ -1,51 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -import '../editable_cell_skeleton/timestamp.dart'; - -class DesktopGridTimestampCellSkin extends IEditableTimestampCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TimestampCellBloc bloc, - TimestampCellState state, - ) { - return Container( - alignment: AlignmentDirectional.centerStart, - child: state.wrap - ? _buildCellContent(state, compactModeNotifier) - : SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - child: _buildCellContent(state, compactModeNotifier), - ), - ); - } - - 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, - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 102b491f52..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:styled_widget/styled_widget.dart'; - -class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TranslateCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - 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, - ), - ), - ), - 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), - ], - ), - ), - ); - }, - ); - }, - ); - } -} - -class TranslateMouseNotifier extends ChangeNotifier { - TranslateMouseNotifier(); - - bool _onEnter = false; - - set onEnter(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get onEnter => _onEnter; -} 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 deleted file mode 100644 index 935716e686..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.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/database/application/cell/bloc/url_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.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/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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_skeleton/url.dart'; - -class DesktopGridURLSkin extends IEditableURLCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - URLCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - URLCellDataNotifier cellDataNotifier, - ) { - 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, - ), - onTapOutside: (_) => focusNode.unfocus(), - ); - }, - ), - ); - } - - @override - List accessoryBuilder( - GridCellAccessoryBuildContext context, - URLCellDataNotifier cellDataNotifier, - ) { - return [ - accessoryFromType( - GridURLCellAccessoryType.visitURL, - cellDataNotifier, - ), - accessoryFromType( - GridURLCellAccessoryType.copyURL, - cellDataNotifier, - ), - ]; - } -} - -GridCellAccessoryBuilder accessoryFromType( - GridURLCellAccessoryType ty, - URLCellDataNotifier cellDataNotifier, -) { - switch (ty) { - case GridURLCellAccessoryType.visitURL: - return VisitURLCellAccessoryBuilder( - builder: (Key key) => _VisitURLAccessory( - key: key, - cellDataNotifier: cellDataNotifier, - ), - ); - case GridURLCellAccessoryType.copyURL: - return CopyURLCellAccessoryBuilder( - builder: (Key key) => _CopyURLAccessory( - key: key, - cellDataNotifier: cellDataNotifier, - ), - ); - } -} - -enum GridURLCellAccessoryType { - copyURL, - visitURL, -} - -typedef CopyURLCellAccessoryBuilder - = GridCellAccessoryBuilder>; - -class _CopyURLAccessory extends StatefulWidget { - const _CopyURLAccessory({ - super.key, - required this.cellDataNotifier, - }); - - final URLCellDataNotifier cellDataNotifier; - - @override - State<_CopyURLAccessory> createState() => _CopyURLAccessoryState(); -} - -class _CopyURLAccessoryState extends State<_CopyURLAccessory> - with GridCellAccessoryState { - @override - Widget build(BuildContext context) { - if (widget.cellDataNotifier.value.isNotEmpty) { - return FlowyTooltip( - message: LocaleKeys.grid_url_copy.tr(), - preferBelow: false, - child: _URLAccessoryIconContainer( - child: FlowySvg( - FlowySvgs.copy_s, - color: AFThemeExtension.of(context).textColor, - ), - ), - ); - } else { - return const SizedBox.shrink(); - } - } - - @override - void onTap() { - final content = widget.cellDataNotifier.value; - if (content.isEmpty) { - return; - } - Clipboard.setData(ClipboardData(text: content)); - showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); - } -} - -typedef VisitURLCellAccessoryBuilder - = GridCellAccessoryBuilder>; - -class _VisitURLAccessory extends StatefulWidget { - const _VisitURLAccessory({ - super.key, - required this.cellDataNotifier, - }); - - final URLCellDataNotifier cellDataNotifier; - - @override - State<_VisitURLAccessory> createState() => _VisitURLAccessoryState(); -} - -class _VisitURLAccessoryState extends State<_VisitURLAccessory> - with GridCellAccessoryState { - @override - Widget build(BuildContext context) { - if (widget.cellDataNotifier.value.isNotEmpty) { - return FlowyTooltip( - message: LocaleKeys.grid_url_launch.tr(), - preferBelow: false, - child: _URLAccessoryIconContainer( - child: FlowySvg( - FlowySvgs.url_s, - color: AFThemeExtension.of(context).textColor, - ), - ), - ); - } else { - return const SizedBox.shrink(); - } - } - - @override - bool enable() => widget.cellDataNotifier.value.isNotEmpty; - - @override - void onTap() => openUrlCellLink(widget.cellDataNotifier.value); -} - -class _URLAccessoryIconContainer extends StatelessWidget { - const _URLAccessoryIconContainer({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - width: 26, - height: 26, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: Corners.s6Border, - ), - child: FlowyHover( - style: HoverStyle( - backgroundColor: AFThemeExtension.of(context).background, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - ), - 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 deleted file mode 100644 index 1e56c5160e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart +++ /dev/null @@ -1,33 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/checkbox.dart'; - -class DesktopRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - CheckboxCellBloc bloc, - CheckboxCellState state, - ) { - return Container( - alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), - 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_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 deleted file mode 100644 index ab0533819a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ /dev/null @@ -1,449 +0,0 @@ -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: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: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'; - -class DesktopRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - ChecklistCellBloc bloc, - PopoverController popoverController, - ) { - return ChecklistRowDetailCell( - context: context, - cellContainerNotifier: cellContainerNotifier, - bloc: bloc, - popoverController: popoverController, - ); - } -} - -class ChecklistRowDetailCell extends StatefulWidget { - const ChecklistRowDetailCell({ - super.key, - required this.context, - required this.cellContainerNotifier, - required this.bloc, - required this.popoverController, - }); - - final BuildContext context; - final CellContainerNotifier cellContainerNotifier; - final ChecklistCellBloc bloc; - final PopoverController popoverController; - - @override - State createState() => _ChecklistRowDetailCellState(); -} - -class _ChecklistRowDetailCellState extends State { - final phantomTextController = TextEditingController(); - - @override - void dispose() { - phantomTextController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Align( - alignment: AlignmentDirectional.centerStart, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ProgressAndHideCompleteButton( - onToggleHideComplete: () => context - .read() - .add(const ChecklistCellEvent.toggleShowIncompleteOnly()), - ), - 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(); - }, - ), - ], - ), - ); - } -} - -@visibleForTesting -class ProgressAndHideCompleteButton extends StatelessWidget { - const ProgressAndHideCompleteButton({ - super.key, - required this.onToggleHideComplete, - }); - - final VoidCallback onToggleHideComplete; - - @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, - ), - ), - ), - ), - ), - ); - } - - 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, - }); - - 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, - ), - ), - 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 deleted file mode 100644 index f1b5f14975..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart +++ /dev/null @@ -1,70 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin { - @override - 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; - - return AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - 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), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: FlowyText( - text, - color: color, - overflow: TextOverflow.ellipsis, - ), - ), - if (state.cellData.reminderId.isNotEmpty) ...[ - const HSpace(4), - FlowyTooltip( - message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(), - child: const FlowySvg(FlowySvgs.clock_alarm_s), - ), - ], - ], - ), - ), - popupBuilder: (BuildContext popoverContent) { - return DateCellEditor( - cellController: bloc.cellController, - onDismissed: () => cellContainerNotifier.isFocus = false, - ); - }, - onClose: () { - cellContainerNotifier.isFocus = false; - }, - ); - } -} 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 deleted file mode 100644 index e90fc85549..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart +++ /dev/null @@ -1,41 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/number.dart'; - -class DesktopRowDetailNumberCellSkin extends IEditableNumberCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - NumberCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - 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/desktop_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart deleted file mode 100644 index d760d3ac29..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart +++ /dev/null @@ -1,75 +0,0 @@ -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_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 '../editable_cell_skeleton/relation.dart'; - -class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { - @override - 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), - ], - child: const RelationCellEditor(), - ); - }, - child: Container( - alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: state.rows.isEmpty - ? _buildPlaceholder(context) - : _buildRows(context, state.rows), - ), - ); - } - - Widget _buildPlaceholder(BuildContext context) { - return FlowyText( - LocaleKeys.grid_row_textPlaceholder.tr(), - color: Theme.of(context).hintColor, - ); - } - - Widget _buildRows(BuildContext context, List rows) { - return Wrap( - runSpacing: 4.0, - spacing: 4.0, - children: rows.map( - (row) { - final isEmpty = row.name.isEmpty; - return FlowyText( - isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, - color: isEmpty ? Theme.of(context).hintColor : null, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ); - }, - ).toList(), - ); - } -} 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 deleted file mode 100644 index ff84744c27..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart +++ /dev/null @@ -1,76 +0,0 @@ -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_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 '../editable_cell_skeleton/select_option.dart'; - -class DesktopRowDetailSelectOptionCellSkin - extends IEditableSelectOptionCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SelectOptionCellBloc bloc, - PopoverController popoverController, - ) { - return AppFlowyPopover( - controller: popoverController, - constraints: const BoxConstraints.tightFor(width: 300), - margin: EdgeInsets.zero, - asBarrier: true, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - onClose: () => cellContainerNotifier.isFocus = false, - onOpen: () => cellContainerNotifier.isFocus = true, - popupBuilder: (_) => SelectOptionCellEditor( - cellController: bloc.cellController, - ), - child: BlocBuilder( - builder: (context, state) { - return Container( - alignment: AlignmentDirectional.centerStart, - padding: state.selectedOptions.isEmpty - ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0) - : const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0), - child: state.selectedOptions.isEmpty - ? _buildPlaceholder(context) - : _buildOptions(context, state.selectedOptions), - ); - }, - ), - ); - } - - Widget _buildPlaceholder(BuildContext context) { - return FlowyText( - LocaleKeys.grid_row_textPlaceholder.tr(), - color: Theme.of(context).hintColor, - ); - } - - Widget _buildOptions(BuildContext context, List options) { - return Wrap( - runSpacing: 4, - spacing: 4, - children: options.map( - (option) { - return SelectOptionTag( - option: option, - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - ); - }, - ).toList(), - ); - } -} 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 deleted file mode 100644 index 30cd54832d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SummaryCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - 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, - ), - ), - ChangeNotifierProvider.value( - value: cellContainerNotifier, - child: Selector( - selector: (_, notifier) => notifier.isHover, - builder: (context, isHover, child) { - return Visibility( - visible: isHover, - child: Row( - children: [ - const Spacer(), - SummaryCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ), - ], - ), - ); - }, - ), - ), - ], - ), - ); - } -} 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 deleted file mode 100644 index 9511c2f871..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart +++ /dev/null @@ -1,39 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/text.dart'; - -class DesktopRowDetailTextCellSkin extends IEditableTextCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TextCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, - 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/desktop_row_detail_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_time_cell.dart deleted file mode 100644 index ffd68933c9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_time_cell.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/time.dart'; - -class DesktopRowDetailTimeCellSkin extends IEditableTimeCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - TimeCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - 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/desktop_row_detail_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart deleted file mode 100644 index 6fc534f313..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart +++ /dev/null @@ -1,26 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -import '../editable_cell_skeleton/timestamp.dart'; - -class DesktopRowDetailTimestampCellSkin extends IEditableTimestampCellSkin { - @override - 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( - 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 deleted file mode 100644 index ee9d7e7300..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart +++ /dev/null @@ -1,114 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../editable_cell_skeleton/url.dart'; - -class DesktopRowDetailURLSkin extends IEditableURLCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - URLCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - URLCellDataNotifier cellDataNotifier, - ) { - return LinkTextField( - controller: textEditingController, - focusNode: focusNode, - ); - } - - @override - List accessoryBuilder( - GridCellAccessoryBuildContext context, - URLCellDataNotifier cellDataNotifier, - ) { - return [ - accessoryFromType( - GridURLCellAccessoryType.visitURL, - cellDataNotifier, - ), - ]; - } -} - -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 deleted file mode 100644 index a374417b3d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class DesktopRowDetailTranslateCellSkin extends IEditableTranslateCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TranslateCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - TextField( - controller: textEditingController, - focusNode: focusNode, - readOnly: true, - 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, - ), - ), - ChangeNotifierProvider.value( - value: cellContainerNotifier, - child: Selector( - selector: (_, notifier) => notifier.isHover, - builder: (context, isHover, child) { - return Visibility( - visible: isHover, - child: Row( - children: [ - const Spacer(), - TranslateCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ), - ], - ), - ); - }, - ), - ), - ], - ), - ); - } -} 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 deleted file mode 100755 index e7b5d0d79b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart +++ /dev/null @@ -1,441 +0,0 @@ -import 'package:flutter/material.dart'; -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'; - -import '../row/accessory/cell_accessory.dart'; -import '../row/accessory/cell_shortcuts.dart'; -import '../row/cells/cell_container.dart'; - -import 'editable_cell_skeleton/checkbox.dart'; -import 'editable_cell_skeleton/checklist.dart'; -import 'editable_cell_skeleton/date.dart'; -import 'editable_cell_skeleton/number.dart'; -import 'editable_cell_skeleton/relation.dart'; -import 'editable_cell_skeleton/select_option.dart'; -import 'editable_cell_skeleton/summary.dart'; -import 'editable_cell_skeleton/text.dart'; -import 'editable_cell_skeleton/time.dart'; -import 'editable_cell_skeleton/timestamp.dart'; -import 'editable_cell_skeleton/url.dart'; - -enum EditableCellStyle { - desktopGrid, - desktopRowDetail, - mobileGrid, - mobileRowDetail, -} - -/// Build an editable cell widget -class EditableCellBuilder { - EditableCellBuilder({required this.databaseController}); - - final DatabaseController databaseController; - - EditableCellWidget buildStyled( - CellContext cellContext, - EditableCellStyle style, - ) { - final fieldType = databaseController.fieldController - .getField(cellContext.fieldId)! - .fieldType; - final key = ValueKey( - "${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}", - ); - return switch (fieldType) { - FieldType.Checkbox => EditableCheckboxCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableCheckboxCellSkin.fromStyle(style), - key: key, - ), - FieldType.Checklist => EditableChecklistCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableChecklistCellSkin.fromStyle(style), - key: key, - ), - FieldType.CreatedTime => EditableTimestampCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableTimestampCellSkin.fromStyle(style), - key: key, - fieldType: FieldType.CreatedTime, - ), - FieldType.DateTime => EditableDateCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableDateCellSkin.fromStyle(style), - key: key, - ), - FieldType.LastEditedTime => EditableTimestampCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableTimestampCellSkin.fromStyle(style), - key: key, - fieldType: FieldType.LastEditedTime, - ), - FieldType.MultiSelect => EditableSelectOptionCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableSelectOptionCellSkin.fromStyle(style), - key: key, - fieldType: FieldType.MultiSelect, - ), - FieldType.Number => EditableNumberCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableNumberCellSkin.fromStyle(style), - key: key, - ), - FieldType.RichText => EditableTextCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableTextCellSkin.fromStyle(style), - key: key, - ), - FieldType.SingleSelect => EditableSelectOptionCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableSelectOptionCellSkin.fromStyle(style), - key: key, - fieldType: FieldType.SingleSelect, - ), - FieldType.URL => EditableURLCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableURLCellSkin.fromStyle(style), - key: key, - ), - FieldType.Relation => EditableRelationCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableRelationCellSkin.fromStyle(style), - key: key, - ), - FieldType.Summary => EditableSummaryCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableSummaryCellSkin.fromStyle(style), - key: key, - ), - FieldType.Time => EditableTimeCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableTimeCellSkin.fromStyle(style), - key: key, - ), - FieldType.Translate => EditableTranslateCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableTranslateCellSkin.fromStyle(style), - key: key, - ), - FieldType.Media => EditableMediaCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableMediaCellSkin.fromStyle(style), - style: style, - key: key, - ), - _ => throw UnimplementedError(), - }; - } - - EditableCellWidget buildCustom( - CellContext cellContext, { - required EditableCellSkinMap skinMap, - }) { - final DatabaseController(:fieldController) = databaseController; - final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType; - - final key = ValueKey( - "${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}", - ); - assert(skinMap.has(fieldType)); - return switch (fieldType) { - FieldType.Checkbox => EditableCheckboxCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.checkboxSkin!, - key: key, - ), - FieldType.Checklist => EditableChecklistCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.checklistSkin!, - key: key, - ), - FieldType.CreatedTime => EditableTimestampCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.timestampSkin!, - key: key, - fieldType: FieldType.CreatedTime, - ), - FieldType.DateTime => EditableDateCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.dateSkin!, - key: key, - ), - FieldType.LastEditedTime => EditableTimestampCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.timestampSkin!, - key: key, - fieldType: FieldType.LastEditedTime, - ), - FieldType.MultiSelect => EditableSelectOptionCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.selectOptionSkin!, - key: key, - fieldType: FieldType.MultiSelect, - ), - FieldType.Number => EditableNumberCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.numberSkin!, - key: key, - ), - FieldType.RichText => EditableTextCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.textSkin!, - key: key, - ), - FieldType.SingleSelect => EditableSelectOptionCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.selectOptionSkin!, - key: key, - fieldType: FieldType.SingleSelect, - ), - FieldType.URL => EditableURLCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.urlSkin!, - key: key, - ), - FieldType.Relation => EditableRelationCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.relationSkin!, - key: key, - ), - FieldType.Time => EditableTimeCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.timeSkin!, - key: key, - ), - FieldType.Media => EditableMediaCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.mediaSkin!, - style: EditableCellStyle.desktopGrid, - key: key, - ), - _ => throw UnimplementedError(), - }; - } -} - -abstract class CellEditable { - SingleListenerChangeNotifier get requestFocus; - - CellContainerNotifier get cellContainerNotifier; -} - -typedef AccessoryBuilder = List Function( - GridCellAccessoryBuildContext buildContext, -); - -abstract class CellAccessory extends Widget { - const CellAccessory({super.key}); - - AccessoryBuilder? get accessoryBuilder; -} - -abstract class EditableCellWidget extends StatefulWidget - implements CellAccessory, CellEditable, CellShortcuts { - EditableCellWidget({super.key}); - - @override - final CellContainerNotifier cellContainerNotifier = CellContainerNotifier(); - - @override - AccessoryBuilder? get accessoryBuilder => null; - - @override - final requestFocus = SingleListenerChangeNotifier(); - - @override - final Map shortcutHandlers = {}; -} - -abstract class GridCellState extends State { - @override - void initState() { - super.initState(); - widget.requestFocus.addListener(onRequestFocus); - } - - @override - void didUpdateWidget(covariant T oldWidget) { - if (oldWidget != this) { - oldWidget.requestFocus.removeListener(onRequestFocus); - widget.requestFocus.addListener(onRequestFocus); - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - widget.requestFocus.removeListener(onRequestFocus); - widget.requestFocus.dispose(); - super.dispose(); - } - - /// Subclass can override this method to request focus. - void onRequestFocus(); - - String? onCopy() => null; -} - -abstract class GridEditableTextCell - extends GridCellState { - SingleListenerFocusNode get focusNode; - - @override - void initState() { - super.initState(); - widget.shortcutHandlers[CellKeyboardKey.onEnter] = - () => focusNode.unfocus(); - _listenOnFocusNodeChanged(); - } - - @override - void dispose() { - widget.shortcutHandlers.clear(); - focusNode.removeAllListener(); - focusNode.dispose(); - super.dispose(); - } - - @override - void onRequestFocus() { - if (!focusNode.hasFocus && focusNode.canRequestFocus) { - FocusScope.of(context).requestFocus(focusNode); - } - } - - void _listenOnFocusNodeChanged() { - widget.cellContainerNotifier.isFocus = focusNode.hasFocus; - focusNode.setListener(() { - widget.cellContainerNotifier.isFocus = focusNode.hasFocus; - focusChanged(); - }); - } - - Future focusChanged() async {} -} - -class SingleListenerChangeNotifier extends ChangeNotifier { - VoidCallback? _listener; - - @override - void addListener(VoidCallback listener) { - if (_listener != null) { - removeListener(_listener!); - } - _listener = listener; - super.addListener(listener); - } - - @override - void dispose() { - _listener = null; - super.dispose(); - } - - void notify() => notifyListeners(); -} - -class SingleListenerFocusNode extends FocusNode { - VoidCallback? _listener; - - void setListener(VoidCallback listener) { - if (_listener != null) { - removeListener(_listener!); - } - - _listener = listener; - super.addListener(listener); - } - - void removeAllListener() { - if (_listener != null) { - removeListener(_listener!); - } - } - - @override - void dispose() { - removeAllListener(); - super.dispose(); - } -} - -class EditableCellSkinMap { - EditableCellSkinMap({ - this.checkboxSkin, - this.checklistSkin, - this.timestampSkin, - this.dateSkin, - this.selectOptionSkin, - this.numberSkin, - this.textSkin, - this.urlSkin, - this.relationSkin, - this.timeSkin, - this.mediaSkin, - }); - - final IEditableCheckboxCellSkin? checkboxSkin; - final IEditableChecklistCellSkin? checklistSkin; - final IEditableTimestampCellSkin? timestampSkin; - final IEditableDateCellSkin? dateSkin; - final IEditableSelectOptionCellSkin? selectOptionSkin; - final IEditableNumberCellSkin? numberSkin; - final IEditableTextCellSkin? textSkin; - final IEditableURLCellSkin? urlSkin; - final IEditableRelationCellSkin? relationSkin; - final IEditableTimeCellSkin? timeSkin; - final IEditableMediaCellSkin? mediaSkin; - - bool has(FieldType fieldType) { - return switch (fieldType) { - FieldType.Checkbox => checkboxSkin != null, - FieldType.Checklist => checklistSkin != null, - FieldType.CreatedTime || - FieldType.LastEditedTime => - timestampSkin != null, - FieldType.DateTime => dateSkin != null, - FieldType.MultiSelect || - FieldType.SingleSelect => - selectOptionSkin != null, - FieldType.Number => numberSkin != null, - 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 deleted file mode 100644 index ab421b8925..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart +++ /dev/null @@ -1,95 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../desktop_grid/desktop_grid_checkbox_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_checkbox_cell.dart'; -import '../mobile_grid/mobile_grid_checkbox_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_checkbox_cell.dart'; - -abstract class IEditableCheckboxCellSkin { - const IEditableCheckboxCellSkin(); - - factory IEditableCheckboxCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridCheckboxCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailCheckboxCellSkin(), - EditableCellStyle.mobileGrid => MobileGridCheckboxCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailCheckboxCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - CheckboxCellBloc bloc, - CheckboxCellState state, - ); -} - -class EditableCheckboxCell extends EditableCellWidget { - EditableCheckboxCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableCheckboxCellSkin skin; - - @override - GridCellState createState() => _CheckboxCellState(); -} - -class _CheckboxCellState extends GridCellState { - late final cellBloc = CheckboxCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - )..add(const CheckboxCellEvent.initial()); - - @override - void dispose() { - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocBuilder( - builder: (context, state) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - state, - ); - }, - ), - ); - } - - @override - void onRequestFocus() => cellBloc.add(const CheckboxCellEvent.select()); - - @override - String? onCopy() { - if (cellBloc.state.isSelected) { - return "Yes"; - } else { - return "No"; - } - } -} 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 deleted file mode 100644 index fbed429642..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart +++ /dev/null @@ -1,89 +0,0 @@ -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_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../desktop_grid/desktop_grid_checklist_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_checklist_cell.dart'; -import '../mobile_grid/mobile_grid_checklist_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_checklist_cell.dart'; - -abstract class IEditableChecklistCellSkin { - const IEditableChecklistCellSkin(); - - factory IEditableChecklistCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridChecklistCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailChecklistCellSkin(), - EditableCellStyle.mobileGrid => MobileGridChecklistCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailChecklistCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - ChecklistCellBloc bloc, - PopoverController popoverController, - ); -} - -class EditableChecklistCell extends EditableCellWidget { - EditableChecklistCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableChecklistCellSkin skin; - - @override - GridCellState createState() => - GridChecklistCellState(); -} - -class GridChecklistCellState extends GridCellState { - final PopoverController _popover = PopoverController(); - late final cellBloc = ChecklistCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void dispose() { - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - _popover, - ), - ); - } - - @override - void onRequestFocus() { - if (widget.skin is DesktopGridChecklistCellSkin) { - _popover.show(); - } - } -} 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 deleted file mode 100644 index e61c759f48..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart +++ /dev/null @@ -1,141 +0,0 @@ -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_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'; -import '../mobile_grid/mobile_grid_date_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_date_cell.dart'; - -abstract class IEditableDateCellSkin { - const IEditableDateCellSkin(); - - factory IEditableDateCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridDateCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailDateCellSkin(), - EditableCellStyle.mobileGrid => MobileGridDateCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailDateCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - DateCellBloc bloc, - DateCellState state, - PopoverController popoverController, - ); -} - -class EditableDateCell extends EditableCellWidget { - EditableDateCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableDateCellSkin skin; - - @override - GridCellState createState() => _DateCellState(); -} - -class _DateCellState extends GridCellState { - final PopoverController _popover = PopoverController(); - late final cellBloc = DateCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void dispose() { - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocBuilder( - builder: (context, state) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - state, - _popover, - ); - }, - ), - ); - } - - @override - void onRequestFocus() { - _popover.show(); - widget.cellContainerNotifier.isFocus = true; - } - - @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', - }; -} 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 deleted file mode 100644 index 4d2bfdf627..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart +++ /dev/null @@ -1,125 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../desktop_grid/desktop_grid_number_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_number_cell.dart'; -import '../mobile_grid/mobile_grid_number_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_number_cell.dart'; - -abstract class IEditableNumberCellSkin { - const IEditableNumberCellSkin(); - - factory IEditableNumberCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridNumberCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailNumberCellSkin(), - EditableCellStyle.mobileGrid => MobileGridNumberCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailNumberCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - NumberCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ); -} - -class EditableNumberCell extends EditableCellWidget { - EditableNumberCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableNumberCellSkin skin; - - @override - GridEditableTextCell createState() => _NumberCellState(); -} - -class _NumberCellState extends GridEditableTextCell { - late final TextEditingController _textEditingController; - late final cellBloc = NumberCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void initState() { - super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); - } - - @override - void dispose() { - _textEditingController.dispose(); - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocListener( - listener: (context, state) { - if (!focusNode.hasFocus) { - _textEditingController.text = state.content; - } - }, - child: Builder( - builder: (context) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - focusNode, - _textEditingController, - ); - }, - ), - ), - ); - } - - @override - SingleListenerFocusNode focusNode = SingleListenerFocusNode(); - - @override - void onRequestFocus() { - focusNode.requestFocus(); - } - - @override - String? onCopy() => cellBloc.state.content; - - @override - Future focusChanged() async { - if (mounted && - !cellBloc.isClosed && - cellBloc.state.content != _textEditingController.text.trim()) { - cellBloc - .add(NumberCellEvent.updateCell(_textEditingController.text.trim())); - } - return super.focusChanged(); - } -} 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 deleted file mode 100644 index 67ca6275a6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart +++ /dev/null @@ -1,96 +0,0 @@ -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_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../desktop_grid/desktop_grid_relation_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_relation_cell.dart'; -import '../mobile_grid/mobile_grid_relation_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_relation_cell.dart'; - -abstract class IEditableRelationCellSkin { - factory IEditableRelationCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridRelationCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailRelationCellSkin(), - EditableCellStyle.mobileGrid => MobileGridRelationCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailRelationCellSkin(), - }; - } - - const IEditableRelationCellSkin(); - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - RelationCellBloc bloc, - RelationCellState state, - PopoverController popoverController, - ); -} - -class EditableRelationCell extends EditableCellWidget { - EditableRelationCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableRelationCellSkin skin; - - @override - GridCellState createState() => _RelationCellState(); -} - -class _RelationCellState extends GridCellState { - final PopoverController _popover = PopoverController(); - late final cellBloc = RelationCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void dispose() { - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocBuilder( - builder: (context, state) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - state, - _popover, - ); - }, - ), - ); - } - - @override - void onRequestFocus() { - _popover.show(); - widget.cellContainerNotifier.isFocus = true; - } - - @override - String? onCopy() => ""; -} 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 deleted file mode 100644 index f7e8b6f435..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart +++ /dev/null @@ -1,92 +0,0 @@ -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_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../desktop_grid/desktop_grid_select_option_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_select_option_cell.dart'; -import '../mobile_grid/mobile_grid_select_option_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_select_cell_option.dart'; - -abstract class IEditableSelectOptionCellSkin { - const IEditableSelectOptionCellSkin(); - - factory IEditableSelectOptionCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridSelectOptionCellSkin(), - EditableCellStyle.desktopRowDetail => - DesktopRowDetailSelectOptionCellSkin(), - EditableCellStyle.mobileGrid => MobileGridSelectOptionCellSkin(), - EditableCellStyle.mobileRowDetail => - MobileRowDetailSelectOptionCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SelectOptionCellBloc bloc, - PopoverController popoverController, - ); -} - -class EditableSelectOptionCell extends EditableCellWidget { - EditableSelectOptionCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - required this.fieldType, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableSelectOptionCellSkin skin; - - final FieldType fieldType; - - @override - GridCellState createState() => - _SelectOptionCellState(); -} - -class _SelectOptionCellState extends GridCellState { - final PopoverController _popover = PopoverController(); - - late final cellBloc = SelectOptionCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void dispose() { - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - _popover, - ), - ); - } - - @override - void onRequestFocus() => _popover.show(); -} 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 deleted file mode 100644 index 7a086b2a35..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart +++ /dev/null @@ -1,267 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/summary_row_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/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/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'; -import 'package:flowy_infra_ui/style_widget/icon_button.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:flutter_bloc/flutter_bloc.dart'; - -abstract class IEditableSummaryCellSkin { - const IEditableSummaryCellSkin(); - - factory IEditableSummaryCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridSummaryCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailSummaryCellSkin(), - EditableCellStyle.mobileGrid => MobileGridSummaryCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailSummaryCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SummaryCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ); -} - -class EditableSummaryCell extends EditableCellWidget { - EditableSummaryCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableSummaryCellSkin skin; - - @override - GridEditableTextCell createState() => - _SummaryCellState(); -} - -class _SummaryCellState extends GridEditableTextCell { - late final TextEditingController _textEditingController; - late final cellBloc = SummaryCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void initState() { - super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); - } - - @override - void dispose() { - _textEditingController.dispose(); - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocListener( - listener: (context, state) { - _textEditingController.text = state.content; - }, - child: Builder( - builder: (context) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - focusNode, - _textEditingController, - ); - }, - ), - ), - ); - } - - @override - SingleListenerFocusNode focusNode = SingleListenerFocusNode(); - - @override - void onRequestFocus() { - focusNode.requestFocus(); - } - - @override - String? onCopy() => cellBloc.state.content; - - @override - Future focusChanged() { - if (mounted && - !cellBloc.isClosed && - cellBloc.state.content != _textEditingController.text.trim()) { - cellBloc - .add(SummaryCellEvent.updateCell(_textEditingController.text.trim())); - } - return super.focusChanged(); - } -} - -class SummaryCellAccessory extends StatelessWidget { - const SummaryCellAccessory({ - required this.viewId, - required this.rowId, - required this.fieldId, - super.key, - }); - - final String viewId; - final String rowId; - final String fieldId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SummaryRowBloc( - viewId: viewId, - 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); - } - } - }, - builder: (context, state) { - return const Row( - children: [SummaryButton(), HSpace(6), CopyButton()], - ); - }, - ), - ); - } -} - -class SummaryButton extends StatelessWidget { - const SummaryButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.loadingState.when( - loading: () { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - }, - finish: () { - return FlowyTooltip( - message: LocaleKeys.tooltip_aiGenerate.tr(), - child: Container( - width: 26, - height: 26, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: Corners.s6Border, - ), - child: FlowyIconButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - fillColor: Theme.of(context).cardColor, - icon: FlowySvg( - FlowySvgs.ai_summary_generate_s, - color: Theme.of(context).colorScheme.primary, - ), - onPressed: () { - context - .read() - .add(const SummaryRowEvent.startSummary()); - }, - ), - ), - ); - }, - ); - }, - ); - } -} - -class CopyButton extends StatelessWidget { - const CopyButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (blocContext, state) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_clickToCopy.tr(), - child: Container( - width: 26, - height: 26, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: Corners.s6Border, - ), - child: FlowyIconButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - fillColor: Theme.of(context).cardColor, - icon: FlowySvg( - FlowySvgs.ai_copy_s, - color: Theme.of(context).colorScheme.primary, - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: state.content)); - showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); - }, - ), - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 3ea622374e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart +++ /dev/null @@ -1,128 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../desktop_grid/desktop_grid_text_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_text_cell.dart'; -import '../mobile_grid/mobile_grid_text_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_text_cell.dart'; - -abstract class IEditableTextCellSkin { - const IEditableTextCellSkin(); - - factory IEditableTextCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridTextCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailTextCellSkin(), - EditableCellStyle.mobileGrid => MobileGridTextCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailTextCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TextCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ); -} - -class EditableTextCell extends EditableCellWidget { - EditableTextCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableTextCellSkin skin; - - @override - GridEditableTextCell createState() => _TextCellState(); -} - -class _TextCellState extends GridEditableTextCell { - late final TextEditingController _textEditingController; - late final cellBloc = TextCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void initState() { - super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); - } - - @override - void dispose() { - _textEditingController.dispose(); - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - 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 ?? ""; - }, - child: Builder( - builder: (context) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - focusNode, - _textEditingController, - ); - }, - ), - ), - ); - } - - @override - SingleListenerFocusNode focusNode = SingleListenerFocusNode(); - - @override - void onRequestFocus() { - focusNode.requestFocus(); - } - - @override - String? onCopy() => cellBloc.state.content; - - @override - Future focusChanged() { - if (mounted && - !cellBloc.isClosed && - cellBloc.state.content != _textEditingController.text.trim()) { - cellBloc - .add(TextCellEvent.updateText(_textEditingController.text.trim())); - } - return super.focusChanged(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart deleted file mode 100644 index 83c34bdf5d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'dart:async'; - -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/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/time_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'; - -import '../desktop_grid/desktop_grid_time_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_time_cell.dart'; -import '../mobile_grid/mobile_grid_time_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_time_cell.dart'; - -abstract class IEditableTimeCellSkin { - const IEditableTimeCellSkin(); - - factory IEditableTimeCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridTimeCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailTimeCellSkin(), - EditableCellStyle.mobileGrid => MobileGridTimeCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailTimeCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - TimeCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ); -} - -class EditableTimeCell extends EditableCellWidget { - EditableTimeCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableTimeCellSkin skin; - - @override - GridEditableTextCell createState() => _TimeCellState(); -} - -class _TimeCellState extends GridEditableTextCell { - late final TextEditingController _textEditingController; - late final cellBloc = TimeCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void initState() { - super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); - } - - @override - void dispose() { - _textEditingController.dispose(); - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocListener( - listener: (context, state) => - _textEditingController.text = state.content, - child: Builder( - builder: (context) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - cellBloc, - focusNode, - _textEditingController, - ); - }, - ), - ), - ); - } - - @override - SingleListenerFocusNode focusNode = SingleListenerFocusNode(); - - @override - void onRequestFocus() { - focusNode.requestFocus(); - } - - @override - String? onCopy() => cellBloc.state.content; - - @override - Future focusChanged() async { - if (mounted && - !cellBloc.isClosed && - cellBloc.state.content != _textEditingController.text.trim()) { - cellBloc - .add(TimeCellEvent.updateCell(_textEditingController.text.trim())); - } - return super.focusChanged(); - } -} 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 deleted file mode 100644 index 2fc9d049cc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart +++ /dev/null @@ -1,94 +0,0 @@ -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_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../desktop_grid/desktop_grid_timestamp_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_timestamp_cell.dart'; -import '../mobile_grid/mobile_grid_timestamp_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_timestamp_cell.dart'; - -abstract class IEditableTimestampCellSkin { - const IEditableTimestampCellSkin(); - - factory IEditableTimestampCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridTimestampCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailTimestampCellSkin(), - EditableCellStyle.mobileGrid => MobileGridTimestampCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailTimestampCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TimestampCellBloc bloc, - TimestampCellState state, - ); -} - -class EditableTimestampCell extends EditableCellWidget { - EditableTimestampCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - required this.fieldType, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableTimestampCellSkin skin; - final FieldType fieldType; - - @override - GridCellState createState() => _TimestampCellState(); -} - -class _TimestampCellState extends GridCellState { - late final cellBloc = TimestampCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void dispose() { - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocBuilder( - builder: (context, state) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - state, - ); - }, - ), - ); - } - - @override - void onRequestFocus() { - widget.cellContainerNotifier.isFocus = true; - } - - @override - String? onCopy() => cellBloc.state.dateStr; -} 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 deleted file mode 100644 index b273419aed..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/translate_row_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/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/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'; -import 'package:flowy_infra_ui/style_widget/icon_button.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:flutter_bloc/flutter_bloc.dart'; - -abstract class IEditableTranslateCellSkin { - const IEditableTranslateCellSkin(); - - factory IEditableTranslateCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridTranslateCellSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailTranslateCellSkin(), - EditableCellStyle.mobileGrid => MobileGridTranslateCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailTranslateCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TranslateCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ); -} - -class EditableTranslateCell extends EditableCellWidget { - EditableTranslateCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableTranslateCellSkin skin; - - @override - GridEditableTextCell createState() => - _TranslateCellState(); -} - -class _TranslateCellState extends GridEditableTextCell { - late final TextEditingController _textEditingController; - late final cellBloc = TranslateCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void initState() { - super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); - } - - @override - void dispose() { - _textEditingController.dispose(); - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocListener( - listener: (context, state) { - _textEditingController.text = state.content; - }, - child: Builder( - builder: (context) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - focusNode, - _textEditingController, - ); - }, - ), - ), - ); - } - - @override - SingleListenerFocusNode focusNode = SingleListenerFocusNode(); - - @override - void onRequestFocus() { - focusNode.requestFocus(); - } - - @override - String? onCopy() => cellBloc.state.content; - - @override - Future focusChanged() { - if (mounted && - !cellBloc.isClosed && - cellBloc.state.content != _textEditingController.text.trim()) { - cellBloc.add( - TranslateCellEvent.updateCell(_textEditingController.text.trim()), - ); - } - return super.focusChanged(); - } -} - -class TranslateCellAccessory extends StatelessWidget { - const TranslateCellAccessory({ - required this.viewId, - required this.rowId, - required this.fieldId, - super.key, - }); - - final String viewId; - final String rowId; - final String fieldId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => TranslateRowBloc( - viewId: viewId, - 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); - } - } - }, - builder: (context, state) { - return const Row( - children: [TranslateButton(), HSpace(6), CopyButton()], - ); - }, - ), - ); - } -} - -class TranslateButton extends StatelessWidget { - const TranslateButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.loadingState.map( - loading: (_) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - }, - finish: (_) { - return FlowyTooltip( - message: LocaleKeys.tooltip_aiGenerate.tr(), - child: Container( - width: 26, - height: 26, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: Corners.s6Border, - ), - child: FlowyIconButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - fillColor: Theme.of(context).cardColor, - icon: FlowySvg( - FlowySvgs.ai_summary_generate_s, - color: Theme.of(context).colorScheme.primary, - ), - onPressed: () { - context - .read() - .add(const TranslateRowEvent.startTranslate()); - }, - ), - ), - ); - }, - ); - }, - ); - } -} - -class CopyButton extends StatelessWidget { - const CopyButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (blocContext, state) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_clickToCopy.tr(), - child: Container( - width: 26, - height: 26, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: Corners.s6Border, - ), - child: FlowyIconButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - fillColor: Theme.of(context).cardColor, - icon: FlowySvg( - FlowySvgs.ai_copy_s, - color: Theme.of(context).colorScheme.primary, - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: state.content)); - showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); - }, - ), - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 39616dbcf8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart +++ /dev/null @@ -1,236 +0,0 @@ -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/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: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:fluttertoast/fluttertoast.dart'; -import 'package:go_router/go_router.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../desktop_grid/desktop_grid_url_cell.dart'; -import '../desktop_row_detail/desktop_row_detail_url_cell.dart'; -import '../mobile_grid/mobile_grid_url_cell.dart'; -import '../mobile_row_detail/mobile_row_detail_url_cell.dart'; - -abstract class IEditableURLCellSkin { - const IEditableURLCellSkin(); - - factory IEditableURLCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => DesktopGridURLSkin(), - EditableCellStyle.desktopRowDetail => DesktopRowDetailURLSkin(), - EditableCellStyle.mobileGrid => MobileGridURLCellSkin(), - EditableCellStyle.mobileRowDetail => MobileRowDetailURLCellSkin(), - }; - } - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - URLCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - URLCellDataNotifier cellDataNotifier, - ); - - List accessoryBuilder( - GridCellAccessoryBuildContext context, - URLCellDataNotifier cellDataNotifier, - ); -} - -typedef URLCellDataNotifier = CellDataNotifier; - -class EditableURLCell extends EditableCellWidget { - EditableURLCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - }) : _cellDataNotifier = CellDataNotifier(value: ''); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableURLCellSkin skin; - final URLCellDataNotifier _cellDataNotifier; - - @override - List Function( - GridCellAccessoryBuildContext buildContext, - ) get accessoryBuilder => (context) { - return skin.accessoryBuilder(context, _cellDataNotifier); - }; - - @override - GridCellState createState() => _GridURLCellState(); -} - -class _GridURLCellState extends GridEditableTextCell { - late final TextEditingController _textEditingController; - late final cellBloc = URLCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - SingleListenerFocusNode focusNode = SingleListenerFocusNode(); - - @override - void initState() { - super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); - } - - @override - void dispose() { - widget._cellDataNotifier.dispose(); - _textEditingController.dispose(); - cellBloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc, - child: BlocListener( - listenWhen: (previous, current) => previous.content != current.content, - listener: (context, state) { - if (!focusNode.hasFocus) { - _textEditingController.value = - _textEditingController.value.copyWith(text: state.content); - } - widget._cellDataNotifier.value = state.content; - }, - child: widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - focusNode, - _textEditingController, - widget._cellDataNotifier, - ), - ), - ); - } - - @override - Future focusChanged() async { - if (mounted && - !cellBloc.isClosed && - cellBloc.state.content != _textEditingController.text) { - cellBloc.add(URLCellEvent.updateURL(_textEditingController.text)); - } - return super.focusChanged(); - } - - @override - String? onCopy() => cellBloc.state.content; -} - -class MobileURLEditor extends StatelessWidget { - const MobileURLEditor({ - super.key, - required this.textEditingController, - }); - - final TextEditingController textEditingController; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const VSpace(4.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: FlowyTextField( - controller: textEditingController, - hintStyle: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Theme.of(context).hintColor), - hintText: LocaleKeys.grid_url_textFieldHint.tr(), - textStyle: Theme.of(context).textTheme.bodyMedium, - keyboardType: TextInputType.url, - hintTextConstraints: const BoxConstraints(maxHeight: 52), - error: context.watch().state.isValid - ? null - : const SizedBox.shrink(), - onChanged: (_) { - if (textEditingController.value.composing.isCollapsed) { - context - .read() - .add(URLCellEvent.updateURL(textEditingController.text)); - } - }, - onSubmitted: (text) => - context.read().add(URLCellEvent.updateURL(text)), - ), - ), - const VSpace(8.0), - MobileQuickActionButton( - enable: context.watch().state.content.isNotEmpty, - onTap: () { - openUrlCellLink(textEditingController.text); - context.pop(); - }, - icon: FlowySvgs.url_s, - text: LocaleKeys.grid_url_launch.tr(), - ), - const MobileQuickActionDivider(), - MobileQuickActionButton( - enable: context.watch().state.content.isNotEmpty, - onTap: () { - Clipboard.setData( - ClipboardData(text: textEditingController.text), - ); - Fluttertoast.showToast( - msg: LocaleKeys.message_copy_success.tr(), - gravity: ToastGravity.BOTTOM, - ); - context.pop(); - }, - icon: FlowySvgs.copy_s, - text: LocaleKeys.grid_url_copy.tr(), - ), - ], - ); - } -} - -void openUrlCellLink(String content) async { - late Uri uri; - - try { - uri = Uri.parse(content); - // `Uri` identifies `localhost` as a scheme - if (!uri.hasScheme || uri.scheme == 'localhost') { - uri = Uri.parse("http://$content"); - await InternetAddress.lookup(uri.host); - } - } catch (_) { - uri = Uri.parse( - "https://www.google.com/search?q=${Uri.encodeComponent(content)}", - ); - } finally { - await launchUrl(uri); - } -} 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 deleted file mode 100644 index e9ac19c874..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart +++ /dev/null @@ -1,29 +0,0 @@ -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:flutter/material.dart'; - -import '../editable_cell_skeleton/checkbox.dart'; - -class MobileGridCheckboxCellSkin extends IEditableCheckboxCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - CheckboxCellBloc bloc, - CheckboxCellState state, - ) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - child: FlowySvg( - state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - size: const Size.square(24), - ), - ), - ); - } -} 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 deleted file mode 100644 index c56d28e1a7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart +++ /dev/null @@ -1,54 +0,0 @@ -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/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: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'; - -class MobileGridChecklistCellSkin extends IEditableChecklistCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - ChecklistCellBloc bloc, - 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(), - ); - }, - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 5686e09295..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart +++ /dev/null @@ -1,60 +0,0 @@ -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: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, - margin: EdgeInsets.zero, - text: Align( - alignment: AlignmentDirectional.centerStart, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - children: [ - if (state.cellData.reminderId.isNotEmpty) ...[ - const FlowySvg(FlowySvgs.clock_alarm_s), - const HSpace(6), - ], - FlowyText( - dateStr, - fontSize: 15, - ), - ], - ), - ), - ), - onTap: () { - showMobileBottomSheet( - context, - builder: (context) { - return MobileDateCellEditScreen( - controller: bloc.cellController, - showAsFullScreen: false, - ); - }, - ); - }, - ); - } -} 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 deleted file mode 100644 index 310c0b5692..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart +++ /dev/null @@ -1,30 +0,0 @@ -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:flutter/material.dart'; - -import '../editable_cell_skeleton/number.dart'; - -class MobileGridNumberCellSkin extends IEditableNumberCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - NumberCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15), - decoration: const InputDecoration( - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), - isCollapsed: true, - ), - onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), - ); - } -} 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 deleted file mode 100644 index 69e9b20104..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart +++ /dev/null @@ -1,53 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/relation.dart'; - -class MobileGridRelationCellSkin extends IEditableRelationCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - RelationCellBloc bloc, - RelationCellState state, - PopoverController popoverController, - ) { - return FlowyButton( - radius: BorderRadius.zero, - hoverColor: Colors.transparent, - margin: EdgeInsets.zero, - text: Align( - alignment: AlignmentDirectional.centerStart, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - children: state.rows - .map( - (row) => FlowyText( - row.name, - fontSize: 15, - decoration: TextDecoration.underline, - ), - ) - .toList(), - ), - ), - ), - onTap: () { - showMobileBottomSheet( - context, - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - builder: (context) { - return const FlowyText("Coming soon"); - }, - ); - }, - ); - } -} 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 deleted file mode 100644 index 010974e49a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart +++ /dev/null @@ -1,69 +0,0 @@ -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/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_backend/protobuf/flowy-database2/select_option_entities.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'; - -import '../editable_cell_skeleton/select_option.dart'; - -class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SelectOptionCellBloc bloc, - PopoverController popoverController, - ) { - return BlocBuilder( - builder: (context, state) { - return FlowyButton( - hoverColor: Colors.transparent, - radius: BorderRadius.zero, - margin: EdgeInsets.zero, - text: Align( - alignment: AlignmentDirectional.centerStart, - child: state.selectedOptions.isEmpty - ? const SizedBox.shrink() - : _buildOptions(context, state.selectedOptions), - ), - onTap: () { - showMobileBottomSheet( - context, - builder: (context) { - return MobileSelectOptionEditor( - cellController: bloc.cellController, - ); - }, - ); - }, - ); - }, - ); - } - - Widget _buildOptions(BuildContext context, List options) { - final children = options - .mapIndexed( - (index, option) => SelectOptionTag( - option: option, - fontSize: 14, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - ), - ) - .toList(); - - return ListView.separated( - scrollDirection: Axis.horizontal, - separatorBuilder: (context, index) => const HSpace(8), - itemCount: children.length, - itemBuilder: (context, index) => children[index], - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), - ); - } -} 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 deleted file mode 100644 index e48c56d74d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:styled_widget/styled_widget.dart'; - -class MobileGridSummaryCellSkin extends IEditableSummaryCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SummaryCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return ChangeNotifierProvider( - create: (_) => SummaryMouseNotifier(), - builder: (context, child) { - 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: Stack( - children: [ - TextField( - controller: textEditingController, - readOnly: true, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - 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: 0), - ], - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 43a4fe49d7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart +++ /dev/null @@ -1,51 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_skeleton/text.dart'; - -class MobileGridTextCellSkin extends IEditableTextCellSkin { - @override - 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, - ), - ), - ), - Expanded( - child: TextField( - controller: textEditingController, - focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 15, - ), - decoration: const InputDecoration( - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 4), - isCollapsed: true, - ), - onTapOutside: (event) => focusNode.unfocus(), - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart deleted file mode 100644 index 08ab04c7c7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/time.dart'; - -class MobileGridTimeCellSkin extends IEditableTimeCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - TimeCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15), - decoration: const InputDecoration( - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), - isCollapsed: true, - ), - onTapOutside: (event) => FocusManager.instance.primaryFocus?.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 deleted file mode 100644 index 68209e7e05..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart +++ /dev/null @@ -1,27 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/timestamp.dart'; - -class MobileGridTimestampCellSkin extends IEditableTimestampCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TimestampCellBloc bloc, - TimestampCellState state, - ) { - return Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - child: FlowyText( - state.dateStr, - fontSize: 15, - overflow: TextOverflow.ellipsis, - ), - ); - } -} 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 deleted file mode 100644 index 4288136734..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:styled_widget/styled_widget.dart'; - -class MobileGridTranslateCellSkin extends IEditableTranslateCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TranslateCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return ChangeNotifierProvider( - create: (_) => TranslateMouseNotifier(), - builder: (context, child) { - 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: Stack( - children: [ - TextField( - controller: textEditingController, - readOnly: true, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - 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: 0), - ], - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 0dbe5474c7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.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:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../editable_cell_skeleton/url.dart'; - -class MobileGridURLCellSkin extends IEditableURLCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - URLCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - URLCellDataNotifier cellDataNotifier, - ) { - return BlocSelector( - selector: (state) => state.content, - builder: (context, content) { - return GestureDetector( - onTap: () => _showURLEditor(context, bloc, textEditingController), - behavior: HitTestBehavior.opaque, - child: Container( - alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - content, - maxLines: 1, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - decoration: TextDecoration.underline, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ); - }, - ); - } - - void _showURLEditor( - BuildContext context, - URLCellBloc bloc, - TextEditingController textEditingController, - ) { - showMobileBottomSheet( - context, - showDragHandle: true, - backgroundColor: AFThemeExtension.of(context).background, - builder: (context) => BlocProvider.value( - value: bloc, - child: MobileURLEditor( - textEditingController: textEditingController, - ), - ), - ); - } - - @override - List>> accessoryBuilder( - GridCellAccessoryBuildContext context, - URLCellDataNotifier cellDataNotifier, - ) => - const []; -} 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 deleted file mode 100644 index ade82e8c5c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart +++ /dev/null @@ -1,43 +0,0 @@ -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:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/checkbox.dart'; - -class MobileRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - CheckboxCellBloc bloc, - CheckboxCellState state, - ) { - return InkWell( - onTap: () => bloc.add(const CheckboxCellEvent.select()), - borderRadius: const BorderRadius.all(Radius.circular(14)), - 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)), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - alignment: AlignmentDirectional.centerStart, - child: FlowySvg( - state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - color: AFThemeExtension.of(context).onBackground, - blendMode: BlendMode.dst, - size: const Size.square(24), - ), - ), - ); - } -} 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 deleted file mode 100644 index 75eee9a560..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart +++ /dev/null @@ -1,70 +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/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: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 '../editable_cell_skeleton/checklist.dart'; - -class MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - ChecklistCellBloc bloc, - PopoverController popoverController, - ) { - return BlocBuilder( - builder: (context, state) { - 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)), - ), - 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 deleted file mode 100644 index 0256ee25cf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart +++ /dev/null @@ -1,70 +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/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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileRowDetailDateCellSkin extends IEditableDateCellSkin { - @override - 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; - - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(14)), - onTap: () => showMobileBottomSheet( - context, - builder: (context) { - return MobileDateCellEditScreen( - controller: bloc.cellController, - showAsFullScreen: false, - ); - }, - ), - 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)), - ), - 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, - ), - ], - ), - ), - ); - } -} 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 deleted file mode 100644 index 430044fb5c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart +++ /dev/null @@ -1,51 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/number.dart'; - -class MobileRowDetailNumberCellSkin extends IEditableNumberCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - NumberCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - keyboardType: const TextInputType.numberWithOptions( - signed: true, - decimal: true, - ), - focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16), - decoration: InputDecoration( - enabledBorder: - _getInputBorder(color: Theme.of(context).colorScheme.outline), - focusedBorder: - _getInputBorder(color: Theme.of(context).colorScheme.primary), - hintText: LocaleKeys.grid_row_textPlaceholder.tr(), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - isCollapsed: true, - isDense: true, - constraints: const BoxConstraints(), - ), - // close keyboard when tapping outside of the text field - onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), - ); - } - - InputBorder _getInputBorder({Color? color}) { - return OutlineInputBorder( - borderSide: BorderSide(color: color!), - borderRadius: const BorderRadius.all(Radius.circular(14)), - gapPadding: 0, - ); - } -} 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 deleted file mode 100644 index c3e8b82867..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart +++ /dev/null @@ -1,55 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileRowDetailRelationCellSkin extends IEditableRelationCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - RelationCellBloc bloc, - RelationCellState state, - PopoverController popoverController, - ) { - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(14)), - onTap: () => showMobileBottomSheet( - context, - builder: (context) { - return const FlowyText("Coming soon"); - }, - ), - 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)), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - child: Wrap( - runSpacing: 4.0, - spacing: 4.0, - children: state.rows - .map( - (row) => FlowyText( - row.name, - fontSize: 16, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ), - ) - .toList(), - ), - ), - ); - } -} 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 deleted file mode 100644 index 7d4eb71f9d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart +++ /dev/null @@ -1,110 +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/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/mobile_select_option_editor.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.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'; - -import '../editable_cell_skeleton/select_option.dart'; - -class MobileRowDetailSelectOptionCellSkin - extends IEditableSelectOptionCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - SelectOptionCellBloc bloc, - PopoverController popoverController, - ) { - return BlocBuilder( - builder: (context, state) { - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(14)), - onTap: () => showMobileBottomSheet( - context, - builder: (context) { - return MobileSelectOptionEditor( - cellController: bloc.cellController, - ); - }, - ), - child: Container( - constraints: const BoxConstraints( - minHeight: 48, - minWidth: double.infinity, - ), - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: state.selectedOptions.isEmpty ? 13 : 10, - ), - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).colorScheme.outline), - ), - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - child: Row( - children: [ - Expanded( - child: state.selectedOptions.isEmpty - ? _buildPlaceholder(context) - : _buildOptions(context, state.selectedOptions), - ), - const HSpace(6), - RotatedBox( - quarterTurns: 3, - child: Icon( - Icons.chevron_left, - color: Theme.of(context).hintColor, - ), - ), - const HSpace(2), - ], - ), - ), - ); - }, - ); - } - - Widget _buildPlaceholder(BuildContext context) { - return Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(vertical: 1), - child: FlowyText( - LocaleKeys.grid_row_textPlaceholder.tr(), - color: Theme.of(context).hintColor, - ), - ); - } - - Widget _buildOptions(BuildContext context, List options) { - final children = options.mapIndexed( - (index, option) { - return Padding( - padding: EdgeInsets.only(left: index == 0 ? 0 : 4), - child: SelectOptionTag( - option: option, - fontSize: 14, - padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5), - ), - ); - }, - ).toList(); - - return Align( - alignment: AlignmentDirectional.centerStart, - child: Wrap( - runSpacing: 4, - children: children, - ), - ); - } -} 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 deleted file mode 100644 index 9974220b96..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; - -class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin { - @override - 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), - ), - 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, - ), - ), - ], - ), - ], - ), - ); - } -} 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 deleted file mode 100644 index fc8f816103..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart +++ /dev/null @@ -1,50 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/text.dart'; - -class MobileRowDetailTextCellSkin extends IEditableTextCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TextCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - maxLines: null, - decoration: InputDecoration( - enabledBorder: - _getInputBorder(color: Theme.of(context).colorScheme.outline), - focusedBorder: - _getInputBorder(color: Theme.of(context).colorScheme.primary), - hintText: LocaleKeys.grid_row_textPlaceholder.tr(), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - isCollapsed: true, - isDense: true, - constraints: const BoxConstraints(minHeight: 48), - hintStyle: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Theme.of(context).hintColor), - ), - onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), - ); - } - - InputBorder _getInputBorder({Color? color}) { - return OutlineInputBorder( - borderSide: BorderSide(color: color!), - borderRadius: const BorderRadius.all(Radius.circular(14)), - gapPadding: 0, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart deleted file mode 100644 index 159f2063a4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/time.dart'; - -class MobileRowDetailTimeCellSkin extends IEditableTimeCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - TimeCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return TextField( - controller: textEditingController, - focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16), - decoration: InputDecoration( - enabledBorder: - _getInputBorder(color: Theme.of(context).colorScheme.outline), - focusedBorder: - _getInputBorder(color: Theme.of(context).colorScheme.primary), - hintText: LocaleKeys.grid_row_textPlaceholder.tr(), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - isCollapsed: true, - isDense: true, - constraints: const BoxConstraints(), - ), - // close keyboard when tapping outside of the text field - onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), - ); - } - - InputBorder _getInputBorder({Color? color}) { - return OutlineInputBorder( - borderSide: BorderSide(color: color!), - borderRadius: const BorderRadius.all(Radius.circular(14)), - gapPadding: 0, - ); - } -} 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 deleted file mode 100644 index f3f800e994..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart +++ /dev/null @@ -1,41 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../editable_cell_skeleton/timestamp.dart'; - -class MobileRowDetailTimestampCellSkin extends IEditableTimestampCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TimestampCellBloc bloc, - TimestampCellState state, - ) { - return 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)), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - child: FlowyText( - state.dateStr.isEmpty - ? LocaleKeys.grid_row_textPlaceholder.tr() - : state.dateStr, - fontSize: 16, - color: state.dateStr.isEmpty ? Theme.of(context).hintColor : null, - maxLines: null, - ), - ); - } -} 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 deleted file mode 100644 index c2d84b3d2e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:flutter/material.dart'; - -class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin { - @override - 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), - ), - 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, - ), - ), - ], - ), - ], - ), - ); - } -} 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 deleted file mode 100644 index 9bb91255aa..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart +++ /dev/null @@ -1,81 +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/database/application/cell/bloc/url_cell_bloc.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: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 '../editable_cell_skeleton/url.dart'; - -class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - URLCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - URLCellDataNotifier cellDataNotifier, - ) { - return BlocSelector( - selector: (state) => state.content, - builder: (context, content) { - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(14)), - onTap: () => showMobileBottomSheet( - context, - showDragHandle: true, - backgroundColor: AFThemeExtension.of(context).background, - builder: (_) { - return BlocProvider.value( - value: bloc, - child: MobileURLEditor( - textEditingController: textEditingController, - ), - ); - }, - ), - 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)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - child: Text( - content.isEmpty - ? LocaleKeys.grid_row_textPlaceholder.tr() - : content, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - decoration: - content.isEmpty ? null : TextDecoration.underline, - color: content.isEmpty - ? Theme.of(context).hintColor - : Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ); - }, - ); - } - - @override - List>> accessoryBuilder( - GridCellAccessoryBuildContext context, - URLCellDataNotifier cellDataNotifier, - ) => - const []; -} 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 deleted file mode 100644 index 9853f9c1bd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ /dev/null @@ -1,469 +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/database/application/cell/cell_controller_builder.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/util/debounce.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: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'; -import 'checklist_progress_bar.dart'; - -class ChecklistCellEditor extends StatefulWidget { - const ChecklistCellEditor({required this.cellController, super.key}); - - final ChecklistCellController cellController; - - @override - State createState() => _ChecklistCellEditorState(); -} - -class _ChecklistCellEditorState extends State { - /// Focus node for the new task text field - late final FocusNode newTaskFocusNode; - - @override - void initState() { - super.initState(); - newTaskFocusNode = FocusNode( - onKeyEvent: (node, event) { - if (event.logicalKey == LogicalKeyboardKey.escape) { - node.unfocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - ); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state.tasks.isEmpty) { - newTaskFocusNode.requestFocus(); - } - }, - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (state.tasks.isNotEmpty) - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), - child: ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - ), - ), - ChecklistItemList( - options: state.tasks, - onUpdateTask: () => newTaskFocusNode.requestFocus(), - ), - if (state.tasks.isNotEmpty) const TypeOptionSeparator(spacing: 0.0), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: NewTaskItem(focusNode: newTaskFocusNode), - ), - ], - ); - }, - ); - } - - @override - void dispose() { - newTaskFocusNode.dispose(); - super.dispose(); - } -} - -/// Displays the a list of all the existing tasks and an input field to create -/// a new task if `isAddingNewTask` is true -class ChecklistItemList extends StatelessWidget { - const ChecklistItemList({ - super.key, - required this.options, - required this.onUpdateTask, - }); - - final List options; - final VoidCallback onUpdateTask; - - @override - Widget build(BuildContext context) { - if (options.isEmpty) { - return const SizedBox.shrink(); - } - - final itemList = options - .mapIndexed( - (index, option) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), - key: ValueKey(option.data.id), - child: ChecklistItem( - task: option, - index: index, - onSubmitted: index == options.length - 1 ? onUpdateTask : null, - ), - ), - ) - .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, - itemBuilder: (context, index) => itemList[index], - itemCount: itemList.length, - padding: const EdgeInsets.symmetric(vertical: 6.0), - onReorder: (from, to) { - context - .read() - .add(ChecklistCellEvent.reorderTask(from, to)); - }, - ), - ); - } -} - -class _SelectTaskIntent extends Intent { - const _SelectTaskIntent(); -} - -class _EndEditingTaskIntent extends Intent { - const _EndEditingTaskIntent(); -} - -class _UpdateTaskDescriptionIntent extends Intent { - const _UpdateTaskDescriptionIntent(); -} - -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; - - @override - State createState() => _ChecklistItemState(); -} - -class _ChecklistItemState extends State { - TextEditingController textController = TextEditingController(); - final textFieldFocusNode = FocusNode(); - final focusNode = FocusNode(skipTraversal: true); - - bool isHovered = false; - bool isFocused = false; - bool isComposing = false; - - final _debounceOnChanged = Debounce( - duration: const Duration(milliseconds: 300), - ); - - @override - void initState() { - super.initState(); - textController.text = widget.task.data.name; - textController.addListener(_onTextChanged); - if (widget.autofocus) { - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - textFieldFocusNode.requestFocus(); - }); - } - } - - 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(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isFocusedOrHovered = isHovered || isFocused; - final color = isFocusedOrHovered || textFieldFocusNode.hasFocus - ? AFThemeExtension.of(context).lightGreyHover - : Colors.transparent; - return FocusableActionDetector( - focusNode: focusNode, - onShowHoverHighlight: (value) => setState(() => isHovered = value), - onFocusChange: (value) => setState(() => isFocused = value), - actions: _buildActions(), - shortcuts: _buildShortcuts(), - child: Container( - constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight), - decoration: BoxDecoration(color: color, borderRadius: Corners.s6Border), - child: _buildChild(isFocusedOrHovered && !textFieldFocusNode.hasFocus), - ), - ); - } - - Widget _buildChild(bool showTrash) { - 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, - onChanged: () { - _debounceOnChanged.call(() { - if (!isComposing) { - _submitUpdateTaskDescription(textController.text); - } - }); - }, - onSubmitted: () { - _submitUpdateTaskDescription(textController.text); - - if (widget.onSubmitted != null) { - widget.onSubmitted?.call(); - } else { - Actions.invoke(context, const NextFocusIntent()); - } - }, - ), - ), - if (showTrash) - ChecklistCellDeleteButton( - 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>( - onInvoke: (_SelectTaskIntent intent) { - context - .read() - .add(ChecklistCellEvent.selectTask(widget.task.data.id)); - return; - }, - ), - _UpdateTaskDescriptionIntent: - CallbackAction<_UpdateTaskDescriptionIntent>( - onInvoke: (_UpdateTaskDescriptionIntent intent) { - textFieldFocusNode.unfocus(); - widget.onSubmitted?.call(); - return; - }, - ), - _EndEditingTaskIntent: CallbackAction<_EndEditingTaskIntent>( - onInvoke: (_EndEditingTaskIntent intent) { - textFieldFocusNode.unfocus(); - return; - }, - ), - }; - } - - void _submitUpdateTaskDescription(String description) => context - .read() - .add(ChecklistCellEvent.updateTaskName(widget.task.data, description)); -} - -/// Creates a new task after entering the description and pressing enter. -/// This can be cancelled by pressing escape -@visibleForTesting -class NewTaskItem extends StatefulWidget { - const NewTaskItem({super.key, required this.focusNode}); - - final FocusNode focusNode; - - @override - State createState() => _NewTaskItemState(); -} - -class _NewTaskItemState extends State { - final textController = TextEditingController(); - - bool isCreateButtonEnabled = false; - bool isComposing = false; - - @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(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight), - child: Row( - 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, - ), - ), - ), - ), - 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, - fontColor: Theme.of(context).colorScheme.onPrimary, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - onPressed: isCreateButtonEnabled - ? () { - context.read().add( - ChecklistCellEvent.createNewTask(textController.text), - ); - widget.focusNode.requestFocus(); - textController.clear(); - } - : null, - ), - ], - ), - ); - } - - 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 deleted file mode 100644 index 789a4adf46..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart +++ /dev/null @@ -1,130 +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/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 '../../application/cell/bloc/checklist_cell_bloc.dart'; - -class ChecklistCellCheckIcon extends StatelessWidget { - const ChecklistCellCheckIcon({ - super.key, - required this.task, - }); - - final ChecklistSelectOption task; - - @override - Widget build(BuildContext context) { - return ExcludeFocus( - child: FlowyIconButton( - width: 32, - icon: FlowySvg( - task.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - ), - hoverColor: Colors.transparent, - onPressed: () => context.read().add( - ChecklistCellEvent.selectTask(task.data.id), - ), - ), - ); - } -} - -class ChecklistCellTextfield extends StatelessWidget { - const ChecklistCellTextfield({ - super.key, - required this.textController, - required this.focusNode, - this.onChanged, - this.contentPadding = const EdgeInsets.symmetric( - vertical: 8, - horizontal: 2, - ), - this.onSubmitted, - }); - - final TextEditingController textController; - final FocusNode focusNode; - final EdgeInsetsGeometry contentPadding; - final VoidCallback? onSubmitted; - final VoidCallback? onChanged; - - @override - Widget build(BuildContext context) { - 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(), - onSubmitted: (_) => onSubmitted?.call(), - ); - } -} - -class ChecklistCellDeleteButton extends StatefulWidget { - const ChecklistCellDeleteButton({ - super.key, - required this.onPressed, - }); - - final VoidCallback onPressed; - - @override - State createState() => - _ChecklistCellDeleteButtonState(); -} - -class _ChecklistCellDeleteButtonState extends State { - final _materialStatesController = WidgetStatesController(); - - @override - void dispose() { - _materialStatesController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextButton( - onPressed: widget.onPressed, - onHover: (_) => setState(() {}), - onFocusChange: (_) => setState(() {}), - style: ButtonStyle( - fixedSize: const WidgetStatePropertyAll(Size.square(32)), - minimumSize: const WidgetStatePropertyAll(Size.square(32)), - maximumSize: const WidgetStatePropertyAll(Size.square(32)), - overlayColor: WidgetStateProperty.resolveWith((state) { - if (state.contains(WidgetState.focused)) { - return AFThemeExtension.of(context).greyHover; - } - return Colors.transparent; - }), - shape: const WidgetStatePropertyAll( - RoundedRectangleBorder(borderRadius: Corners.s6Border), - ), - ), - statesController: _materialStatesController, - child: FlowySvg( - FlowySvgs.delete_s, - color: _materialStatesController.value.contains(WidgetState.hovered) || - _materialStatesController.value.contains(WidgetState.focused) - ? Theme.of(context).colorScheme.error - : null, - ), - ); - } -} 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 deleted file mode 100644 index 7e0b376f77..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart +++ /dev/null @@ -1,81 +0,0 @@ -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 { - const ChecklistProgressBar({ - super.key, - required this.tasks, - required this.percent, - this.textStyle, - }); - - final List tasks; - final double percent; - final TextStyle? textStyle; - final int segmentLimit = 5; - - @override - State createState() => _ChecklistProgressBarState(); -} - -class _ChecklistProgressBarState extends State { - @override - Widget build(BuildContext context) { - final numFinishedTasks = widget.tasks.where((e) => e.isSelected).length; - final completedTaskColor = numFinishedTasks == widget.tasks.length - ? AFThemeExtension.of(context).success - : Theme.of(context).colorScheme.primary; - - 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, - ), - ), - ), - ], - ) - : 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, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: Text( - "${(widget.percent * 100).round()}%", - style: widget.textStyle, - ), - ), - ), - ], - ); - } -} 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/extension.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart deleted file mode 100644 index c29c7a23c1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart +++ /dev/null @@ -1,124 +0,0 @@ -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: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) { - switch (this) { - case SelectOptionColorPB.Purple: - return AFThemeExtension.of(context).tint1; - case SelectOptionColorPB.Pink: - return AFThemeExtension.of(context).tint2; - case SelectOptionColorPB.LightPink: - return AFThemeExtension.of(context).tint3; - case SelectOptionColorPB.Orange: - return AFThemeExtension.of(context).tint4; - case SelectOptionColorPB.Yellow: - return AFThemeExtension.of(context).tint5; - case SelectOptionColorPB.Lime: - return AFThemeExtension.of(context).tint6; - case SelectOptionColorPB.Green: - return AFThemeExtension.of(context).tint7; - case SelectOptionColorPB.Aqua: - return AFThemeExtension.of(context).tint8; - case SelectOptionColorPB.Blue: - return AFThemeExtension.of(context).tint9; - default: - throw ArgumentError; - } - } - - String colorName() { - switch (this) { - case SelectOptionColorPB.Purple: - return LocaleKeys.grid_selectOption_purpleColor.tr(); - case SelectOptionColorPB.Pink: - return LocaleKeys.grid_selectOption_pinkColor.tr(); - case SelectOptionColorPB.LightPink: - return LocaleKeys.grid_selectOption_lightPinkColor.tr(); - case SelectOptionColorPB.Orange: - return LocaleKeys.grid_selectOption_orangeColor.tr(); - case SelectOptionColorPB.Yellow: - return LocaleKeys.grid_selectOption_yellowColor.tr(); - case SelectOptionColorPB.Lime: - return LocaleKeys.grid_selectOption_limeColor.tr(); - case SelectOptionColorPB.Green: - return LocaleKeys.grid_selectOption_greenColor.tr(); - case SelectOptionColorPB.Aqua: - return LocaleKeys.grid_selectOption_aquaColor.tr(); - case SelectOptionColorPB.Blue: - return LocaleKeys.grid_selectOption_blueColor.tr(); - default: - throw ArgumentError; - } - } -} - -class SelectOptionTag extends StatelessWidget { - const SelectOptionTag({ - super.key, - this.option, - this.name, - this.fontSize, - this.color, - this.textStyle, - this.onRemove, - this.textAlign, - this.isExpanded = false, - this.borderRadius, - required this.padding, - }) : assert(option != null || name != null && color != null); - - final SelectOptionPB? option; - final String? name; - final double? fontSize; - final Color? color; - final TextStyle? textStyle; - final void Function(String)? onRemove; - final EdgeInsets padding; - final BorderRadius? borderRadius; - final TextAlign? textAlign; - final bool isExpanded; - - @override - Widget build(BuildContext context) { - final optionName = option?.name ?? name!; - final optionColor = option?.color.toColor(context) ?? color!; - final text = FlowyText( - optionName, - fontSize: fontSize, - overflow: TextOverflow.ellipsis, - color: AFThemeExtension.of(context).textColor, - textAlign: textAlign, - ); - - return Container( - padding: onRemove == null ? padding : padding.copyWith(right: 2.0), - decoration: BoxDecoration( - color: optionColor, - borderRadius: borderRadius ?? - BorderRadius.circular(UniversalPlatform.isDesktopOrWeb ? 6 : 11), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - isExpanded ? Expanded(child: text) : Flexible(child: text), - if (onRemove != null) ...[ - const HSpace(4), - FlowyIconButton( - width: 16.0, - onPressed: () => onRemove?.call(optionName), - hoverColor: Colors.transparent, - icon: const FlowySvg(FlowySvgs.close_s), - ), - ], - ], - ), - ); - } -} 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 deleted file mode 100644 index 6e86d88e5b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart +++ /dev/null @@ -1,326 +0,0 @@ -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'; -import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_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_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class MobileChecklistCellEditScreen extends StatefulWidget { - const MobileChecklistCellEditScreen({super.key}); - - @override - State createState() => - _MobileChecklistCellEditScreenState(); -} - -class _MobileChecklistCellEditScreenState - extends State { - @override - 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()), - ], - ), - ); - } - - Widget _buildHeader(BuildContext context) { - return Stack( - children: [ - SizedBox( - height: 44.0, - child: Align( - child: FlowyText.medium( - LocaleKeys.grid_field_checklistFieldName.tr(), - fontSize: 18, - ), - ), - ), - ], - ); - } -} - -class _TaskList extends StatelessWidget { - const _TaskList(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final cells = []; - cells.addAll( - 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)); - }, - ), - ) - .toList(), - ); - cells.add( - const _NewTaskButton(key: ValueKey('mobile_checklist_new_task')), - ); - - return ReorderableListView.builder( - shrinkWrap: true, - proxyDecorator: (child, index, _) => Material( - color: Colors.transparent, - child: BlocProvider.value( - value: context.read(), - child: child, - ), - ), - buildDefaultDragHandles: false, - itemCount: cells.length, - itemBuilder: (_, index) => cells[index], - padding: const EdgeInsets.only(bottom: 12.0), - onReorder: (from, to) { - context - .read() - .add(ChecklistCellEvent.reorderTask(from, to)); - }, - ); - }, - ); - } -} - -class _ChecklistItem extends StatefulWidget { - const _ChecklistItem({ - super.key, - required this.task, - required this.index, - required this.autofocus, - this.onAutofocus, - }); - - 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(); - Timer? _debounceOnChanged; - - @override - void initState() { - super.initState(); - textController = TextEditingController(text: widget.task.data.name); - if (widget.autofocus) { - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - widget.onAutofocus?.call(); - }); - } - } - - @override - void didUpdateWidget(covariant oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.task.data.name != oldWidget.task.data.name && - !focusNode.hasFocus) { - textController.text = widget.task.data.name; - } - } - - @override - void 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), - 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, - ), - ), - ), - ), - ), - Expanded( - child: TextField( - controller: textController, - focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium, - keyboardType: TextInputType.multiline, - maxLines: null, - decoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - isCollapsed: true, - isDense: true, - contentPadding: const EdgeInsets.symmetric(vertical: 12), - hintText: LocaleKeys.grid_checklist_taskHint.tr(), - ), - onChanged: _debounceOnChangedText, - onSubmitted: (description) { - _submitUpdateTaskDescription(description); - }, - ), - ), - InkWell( - borderRadius: BorderRadius.circular(22), - onTap: _showDeleteTaskBottomSheet, - child: SizedBox.square( - dimension: 44, - child: Center( - child: FlowySvg( - FlowySvgs.three_dots_s, - color: Theme.of(context).hintColor, - ), - ), - ), - ), - ], - ), - ); - } - - void _debounceOnChangedText(String text) { - _debounceOnChanged?.cancel(); - _debounceOnChanged = Timer(const Duration(milliseconds: 300), () { - _submitUpdateTaskDescription(text); - }); - } - - void _submitUpdateTaskDescription(String description) { - context.read().add( - ChecklistCellEvent.updateTaskName( - widget.task.data, - description.trim(), - ), - ); - } - - void _showDeleteTaskBottomSheet() { - showMobileBottomSheet( - context, - showDragHandle: true, - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: InkWell( - onTap: () { - context.read().add( - ChecklistCellEvent.deleteTask(widget.task.data.id), - ); - context.pop(); - }, - borderRadius: BorderRadius.circular(12), - child: Container( - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - FlowySvg( - FlowySvgs.m_delete_m, - size: const Size.square(20), - color: Theme.of(context).colorScheme.error, - ), - const HSpace(8), - FlowyText( - LocaleKeys.button_delete.tr(), - fontSize: 15, - color: Theme.of(context).colorScheme.error, - ), - ], - ), - ), - ), - ), - const Divider(height: 9), - ], - ), - ); - } -} - -class _NewTaskButton extends StatelessWidget { - const _NewTaskButton({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - context - .read() - .add(const ChecklistCellEvent.updatePhantomIndex(-1)); - context - .read() - .add(const ChecklistCellEvent.createNewTask("")); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 13), - child: Row( - children: [ - const FlowySvg(FlowySvgs.add_s, size: Size.square(20)), - const HSpace(11), - FlowyText(LocaleKeys.grid_checklist_addNew.tr(), fontSize: 15), - ], - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index bd84c9074d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart +++ /dev/null @@ -1,552 +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:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; -import 'package:appflowy/mobile/presentation/base/option_color_list.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/plugins/base/drag_handler.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/plugins/database/widgets/cell_editor/extension.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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:protobuf/protobuf.dart'; - -// include single select and multiple select -class MobileSelectOptionEditor extends StatefulWidget { - const MobileSelectOptionEditor({ - super.key, - required this.cellController, - }); - - final SelectOptionCellController cellController; - - @override - State createState() => - _MobileSelectOptionEditorState(); -} - -class _MobileSelectOptionEditorState extends State { - final searchController = TextEditingController(); - final renameController = TextEditingController(); - - String typingOption = ''; - FieldType get fieldType => widget.cellController.fieldType; - - bool showMoreOptions = false; - SelectOptionPB? option; - - @override - void dispose() { - searchController.dispose(); - renameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints.tightFor(height: 420), - child: BlocProvider( - create: (context) => SelectOptionCellEditorBloc( - cellController: widget.cellController, - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DragHandle(), - _buildHeader(context), - const Divider(height: 0.5), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: showMoreOptions ? 0.0 : 16.0, - ), - child: _buildBody(context), - ), - ), - ], - ); - }, - ), - ), - ); - } - - Widget _buildHeader(BuildContext context) { - const height = 44.0; - return Stack( - children: [ - if (showMoreOptions) - Align( - alignment: Alignment.centerLeft, - child: AppBarBackButton(onTap: _popOrBack), - ), - SizedBox( - height: 44.0, - child: Align( - child: FlowyText.medium( - _headerTitle(), - fontSize: 18, - ), - ), - ), - ].map((e) => SizedBox(height: height, child: e)).toList(), - ); - } - - Widget _buildBody(BuildContext context) { - if (showMoreOptions && option != null) { - return _MoreOptions( - initialOption: option!, - controller: renameController, - onDelete: () { - context - .read() - .add(SelectOptionCellEditorEvent.deleteOption(option!)); - _popOrBack(); - }, - onUpdate: (name, color) { - final option = this.option; - if (option == null) { - return; - } - option.freeze(); - context.read().add( - SelectOptionCellEditorEvent.updateOption( - option.rebuild((p0) { - if (name != null) { - p0.name = name; - } - if (color != null) { - p0.color = color; - } - }), - ), - ); - }, - ); - } - - return SingleChildScrollView( - child: Column( - children: [ - const VSpace(16), - _SearchField( - controller: searchController, - hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(), - onSubmitted: (_) { - context - .read() - .add(const SelectOptionCellEditorEvent.submitTextField()); - searchController.clear(); - }, - onChanged: (value) { - typingOption = value; - context.read().add( - SelectOptionCellEditorEvent.selectMultipleOptions( - [], - value, - ), - ); - }, - ), - const VSpace(22), - _OptionList( - fieldType: widget.cellController.fieldType, - onCreateOption: (optionName) { - context - .read() - .add(const SelectOptionCellEditorEvent.createOption()); - searchController.clear(); - }, - onCheck: (option, isSelected) { - if (isSelected) { - context - .read() - .add(SelectOptionCellEditorEvent.unselectOption(option.id)); - } else { - context - .read() - .add(SelectOptionCellEditorEvent.selectOption(option.id)); - } - }, - onMoreOptions: (option) { - setState(() { - this.option = option; - renameController.text = option.name; - showMoreOptions = true; - }); - }, - ), - ], - ), - ); - } - - String _headerTitle() { - switch (fieldType) { - case FieldType.SingleSelect: - return LocaleKeys.grid_field_singleSelectFieldName.tr(); - case FieldType.MultiSelect: - return LocaleKeys.grid_field_multiSelectFieldName.tr(); - default: - throw UnimplementedError(); - } - } - - void _popOrBack() { - if (showMoreOptions) { - setState(() { - showMoreOptions = false; - option = null; - }); - } else { - context.pop(); - } - } -} - -class _SearchField extends StatelessWidget { - const _SearchField({ - this.hintText, - required this.onChanged, - required this.onSubmitted, - required this.controller, - }); - - final String? hintText; - final void Function(String value) onChanged; - final void Function(String value) onSubmitted; - final TextEditingController controller; - - @override - Widget build(BuildContext context) { - return FlowyMobileSearchTextField( - controller: controller, - onChanged: onChanged, - onSubmitted: onSubmitted, - hintText: hintText, - ); - } -} - -class _OptionList extends StatelessWidget { - const _OptionList({ - required this.fieldType, - required this.onCreateOption, - required this.onCheck, - required this.onMoreOptions, - }); - - final FieldType fieldType; - final void Function(String optionName) onCreateOption; - final void Function(SelectOptionPB option, bool value) onCheck; - final void Function(SelectOptionPB option) onMoreOptions; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - // existing options - final List cells = []; - - // create an option cell - if (state.createSelectOptionSuggestion != null) { - cells.add( - _CreateOptionCell( - name: state.createSelectOptionSuggestion!.name, - color: state.createSelectOptionSuggestion!.color, - onTap: () => onCreateOption( - state.createSelectOptionSuggestion!.name, - ), - ), - ); - } - - cells.addAll( - state.options.map( - (option) => MobileSelectOption( - indicator: fieldType == FieldType.MultiSelect - ? MobileSelectedOptionIndicator.multi - : MobileSelectedOptionIndicator.single, - option: option, - isSelected: state.selectedOptions.contains(option), - onTap: (value) => onCheck(option, value), - onMoreOptions: () => onMoreOptions(option), - ), - ), - ); - - return ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - separatorBuilder: (_, __) => const VSpace(20), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (_, int index) => cells[index], - padding: const EdgeInsets.only(bottom: 12.0), - ); - }, - ); - } -} - -class MobileSelectOption extends StatelessWidget { - const MobileSelectOption({ - super.key, - required this.indicator, - required this.option, - required this.isSelected, - required this.onTap, - this.showMoreOptionsButton = true, - this.onMoreOptions, - }); - - final MobileSelectedOptionIndicator indicator; - final SelectOptionPB option; - final bool isSelected; - final void Function(bool value) onTap; - final bool showMoreOptionsButton; - final VoidCallback? onMoreOptions; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 40, - child: GestureDetector( - // no need to add click effect, so using gesture detector - behavior: HitTestBehavior.translucent, - onTap: () => onTap(isSelected), - child: Row( - children: [ - // checked or selected icon - SizedBox( - height: 20, - width: 20, - child: _IsSelectedIndicator( - indicator: indicator, - isSelected: isSelected, - ), - ), - // padding - const HSpace(12), - // option tag - Expanded( - child: Align( - alignment: AlignmentDirectional.centerStart, - child: SelectOptionTag( - option: option, - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 14, - ), - textAlign: TextAlign.center, - fontSize: 15.0, - ), - ), - ), - if (showMoreOptionsButton) ...[ - const HSpace(24), - // more options - FlowyIconButton( - icon: const FlowySvg( - FlowySvgs.m_field_more_s, - ), - onPressed: onMoreOptions, - ), - ], - ], - ), - ), - ); - } -} - -class _CreateOptionCell extends StatelessWidget { - const _CreateOptionCell({ - required this.name, - required this.color, - required this.onTap, - }); - - final String name; - final SelectOptionColorPB color; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 44, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onTap, - child: Row( - children: [ - FlowyText( - LocaleKeys.grid_selectOption_create.tr(), - color: Theme.of(context).hintColor, - ), - const HSpace(8), - Expanded( - child: Align( - alignment: AlignmentDirectional.centerStart, - child: SelectOptionTag( - name: name, - color: color.toColor(context), - textAlign: TextAlign.center, - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 14, - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -class _MoreOptions extends StatefulWidget { - const _MoreOptions({ - required this.initialOption, - required this.onDelete, - required this.onUpdate, - required this.controller, - }); - - final SelectOptionPB initialOption; - final VoidCallback onDelete; - final void Function(String? name, SelectOptionColorPB? color) onUpdate; - final TextEditingController controller; - - @override - State<_MoreOptions> createState() => _MoreOptionsState(); -} - -class _MoreOptionsState extends State<_MoreOptions> { - late SelectOptionPB option = widget.initialOption; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildRenameTextField(context), - const VSpace(16.0), - _buildDeleteButton(context), - const VSpace(16.0), - Padding( - padding: const EdgeInsets.only(left: 12.0), - child: FlowyText( - LocaleKeys.grid_selectOption_colorPanelTitle.tr().toUpperCase(), - color: Theme.of(context).hintColor, - fontSize: 13, - ), - ), - const VSpace(4.0), - FlowyOptionDecorateBox( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, - horizontal: 6.0, - ), - child: OptionColorList( - selectedColor: option.color, - onSelectedColor: (color) { - widget.onUpdate(null, color); - setState(() { - option.freeze(); - option = option.rebuild((option) => option.color = color); - }); - }, - ), - ), - ), - ], - ), - ); - } - - Widget _buildRenameTextField(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints.tightFor(height: 52.0), - child: FlowyOptionTile.textField( - showTopBorder: false, - onTextChanged: (name) => widget.onUpdate(name, null), - controller: widget.controller, - ), - ); - } - - Widget _buildDeleteButton(BuildContext context) { - return FlowyOptionTile.text( - text: LocaleKeys.button_delete.tr(), - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.m_delete_s, - color: Theme.of(context).colorScheme.error, - ), - onTap: widget.onDelete, - ); - } -} - -enum MobileSelectedOptionIndicator { single, multi } - -class _IsSelectedIndicator extends StatelessWidget { - const _IsSelectedIndicator({ - required this.indicator, - required this.isSelected, - }); - - final MobileSelectedOptionIndicator indicator; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return isSelected - ? DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.primary, - ), - child: Center( - child: indicator == MobileSelectedOptionIndicator.multi - ? FlowySvg( - FlowySvgs.checkmark_tiny_s, - color: Theme.of(context).colorScheme.onPrimary, - ) - : Container( - width: 7.5, - height: 7.5, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - ), - ) - : DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).dividerColor, - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index 7f6960de9d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ /dev/null @@ -1,564 +0,0 @@ -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: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:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../application/cell/bloc/relation_cell_bloc.dart'; -import '../../application/cell/bloc/relation_row_search_bloc.dart'; - -class RelationCellEditor extends StatelessWidget { - const RelationCellEditor({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, cellState) { - return cellState.relatedDatabaseMeta == null - ? const _RelationCellEditorDatabasePicker() - : _RelationCellEditorContent( - relatedDatabaseMeta: cellState.relatedDatabaseMeta!, - selectedRowIds: cellState.rows.map((e) => e.rowId).toList(), - ); - }, - ); - } -} - -class _RelationCellEditorContent extends StatefulWidget { - const _RelationCellEditorContent({ - required this.relatedDatabaseMeta, - required this.selectedRowIds, - }); - - final DatabaseMeta relatedDatabaseMeta; - final List selectedRowIds; - - @override - State<_RelationCellEditorContent> createState() => - _RelationCellEditorContentState(); -} - -class _RelationCellEditorContentState - extends State<_RelationCellEditorContent> { - final textEditingController = TextEditingController(); - late final FocusNode focusNode; - late final bloc = RelationRowSearchBloc( - databaseId: widget.relatedDatabaseMeta.databaseId, - ); - - @override - void initState() { - super.initState(); - focusNode = FocusNode( - onKeyEvent: (node, event) { - switch (event.logicalKey) { - case LogicalKeyboardKey.arrowUp when event is! KeyUpEvent: - if (textEditingController.value.composing.isCollapsed) { - bloc.add(const RelationRowSearchEvent.focusPreviousOption()); - return KeyEventResult.handled; - } - break; - case LogicalKeyboardKey.arrowDown when event is! KeyUpEvent: - if (textEditingController.value.composing.isCollapsed) { - bloc.add(const RelationRowSearchEvent.focusNextOption()); - return KeyEventResult.handled; - } - break; - case LogicalKeyboardKey.escape when event is! KeyUpEvent: - if (!textEditingController.value.composing.isCollapsed) { - final end = textEditingController.value.composing.end; - final text = textEditingController.text; - - textEditingController.value = TextEditingValue( - text: text, - selection: TextSelection.collapsed(offset: end), - ); - return KeyEventResult.handled; - } - break; - } - return KeyEventResult.ignored; - }, - ); - } - - @override - void dispose() { - textEditingController.dispose(); - focusNode.dispose(); - bloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: bloc), - BlocProvider.value(value: context.read()), - ], - child: BlocBuilder( - buildWhen: (previous, current) => - !listEquals(previous.filteredRows, current.filteredRows), - builder: (context, state) { - final selected = []; - final unselected = []; - for (final row in state.filteredRows) { - if (widget.selectedRowIds.contains(row.rowId)) { - selected.add(row); - } else { - unselected.add(row); - } - } - return TextFieldTapRegion( - child: CustomScrollView( - shrinkWrap: true, - slivers: [ - _CellEditorTitle( - databaseMeta: widget.relatedDatabaseMeta, - ), - _SearchField( - focusNode: focusNode, - textEditingController: textEditingController, - ), - const SliverToBoxAdapter( - child: TypeOptionSeparator(spacing: 0.0), - ), - if (state.filteredRows.isEmpty) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(6.0) + - GridSize.typeOptionContentInsets, - child: FlowyText.regular( - LocaleKeys.grid_relation_emptySearchResult.tr(), - color: Theme.of(context).hintColor, - ), - ), - ), - if (selected.isNotEmpty) ...[ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0) + - GridSize.typeOptionContentInsets, - child: FlowyText.regular( - LocaleKeys.grid_relation_linkedRowListLabel.plural( - selected.length, - namedArgs: {'count': '${selected.length}'}, - ), - fontSize: 11, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, - ), - ), - ), - _RowList( - databaseId: widget.relatedDatabaseMeta.databaseId, - rows: selected, - isSelected: true, - ), - const SliverToBoxAdapter( - child: VSpace(4.0), - ), - ], - if (unselected.isNotEmpty) ...[ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0) + - GridSize.typeOptionContentInsets, - child: FlowyText.regular( - LocaleKeys.grid_relation_unlinkedRowListLabel.tr(), - fontSize: 11, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, - ), - ), - ), - _RowList( - databaseId: widget.relatedDatabaseMeta.databaseId, - rows: unselected, - isSelected: false, - ), - const SliverToBoxAdapter( - child: VSpace(4.0), - ), - ], - ], - ), - ); - }, - ), - ); - } -} - -class _CellEditorTitle extends StatelessWidget { - const _CellEditorTitle({ - required this.databaseMeta, - }); - - final DatabaseMeta databaseMeta; - - @override - Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0) + - GridSize.typeOptionContentInsets, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText.regular( - LocaleKeys.grid_relation_inRelatedDatabase.tr(), - 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, - ), - ), - ), - ), - ], - ), - ), - ); - } - - 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 { - const _SearchField({ - required this.focusNode, - required this.textEditingController, - }); - - final FocusNode focusNode; - final TextEditingController textEditingController; - - @override - Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(left: 6.0, bottom: 6.0, right: 6.0), - child: FlowyTextField( - focusNode: focusNode, - controller: textEditingController, - hintText: LocaleKeys.grid_relation_rowSearchTextFieldPlaceholder.tr(), - hintStyle: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: Theme.of(context).hintColor), - onChanged: (text) { - if (textEditingController.value.composing.isCollapsed) { - context - .read() - .add(RelationRowSearchEvent.updateFilter(text)); - } - }, - onSubmitted: (_) { - final focusedRowId = - context.read().state.focusedRowId; - if (focusedRowId != null) { - final row = context - .read() - .state - .rows - .firstWhereOrNull((e) => e.rowId == focusedRowId); - if (row != null) { - FlowyOverlay.show( - context: context, - builder: (BuildContext overlayContext) { - return BlocProvider.value( - value: context.read(), - child: RelatedRowDetailPage( - databaseId: context - .read() - .state - .relatedDatabaseMeta! - .databaseId, - rowId: row.rowId, - ), - ); - }, - ); - PopoverContainer.of(context).close(); - } else { - context - .read() - .add(RelationCellEvent.selectRow(focusedRowId)); - } - } - focusNode.requestFocus(); - }, - ), - ), - ); - } -} - -class _RowList extends StatelessWidget { - const _RowList({ - required this.databaseId, - required this.rows, - required this.isSelected, - }); - - final String databaseId; - final List rows; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => _RowListItem( - row: rows[index], - databaseId: databaseId, - isSelected: isSelected, - ), - childCount: rows.length, - ), - ); - } -} - -class _RowListItem extends StatelessWidget { - const _RowListItem({ - required this.row, - required this.isSelected, - required this.databaseId, - }); - - final RelatedRowDataPB row; - final String databaseId; - final bool isSelected; - - @override - Widget build(BuildContext context) { - final isHovered = - context.watch().state.focusedRowId == row.rowId; - return Container( - height: 28, - margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0), - decoration: BoxDecoration( - color: isHovered ? AFThemeExtension.of(context).lightGreyHover : null, - borderRadius: const BorderRadius.all(Radius.circular(6)), - ), - 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, - ), - ); - }, - ); - PopoverContainer.of(context).close(); - } else { - context - .read() - .add(RelationCellEvent.selectRow(row.rowId)); - } - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onHover: (_) => context - .read() - .add(RelationRowSearchEvent.updateFocusedOption(row.rowId)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4.0), - child: Row( - children: [ - Expanded( - child: FlowyText( - row.name.trim().isEmpty - ? LocaleKeys.grid_title_placeholder.tr() - : row.name, - color: row.name.trim().isEmpty - ? Theme.of(context).hintColor - : null, - overflow: TextOverflow.ellipsis, - ), - ), - if (isSelected && isHovered) - _UnselectRowButton( - onPressed: () => context - .read() - .add(RelationCellEvent.selectRow(row.rowId)), - ), - ], - ), - ), - ), - ), - ); - } -} - -class _UnselectRowButton extends StatefulWidget { - const _UnselectRowButton({ - required this.onPressed, - }); - - final VoidCallback onPressed; - - @override - State<_UnselectRowButton> createState() => _UnselectRowButtonState(); -} - -class _UnselectRowButtonState extends State<_UnselectRowButton> { - final _materialStatesController = WidgetStatesController(); - - @override - void dispose() { - _materialStatesController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextButton( - onPressed: widget.onPressed, - onHover: (_) => setState(() {}), - onFocusChange: (_) => setState(() {}), - style: ButtonStyle( - fixedSize: const WidgetStatePropertyAll(Size.square(32)), - minimumSize: const WidgetStatePropertyAll(Size.square(32)), - maximumSize: const WidgetStatePropertyAll(Size.square(32)), - overlayColor: WidgetStateProperty.resolveWith((state) { - if (state.contains(WidgetState.focused)) { - return AFThemeExtension.of(context).greyHover; - } - return Colors.transparent; - }), - shape: const WidgetStatePropertyAll( - RoundedRectangleBorder(borderRadius: Corners.s6Border), - ), - ), - statesController: _materialStatesController, - child: Container( - color: _materialStatesController.value.contains(WidgetState.hovered) || - _materialStatesController.value.contains(WidgetState.focused) - ? Theme.of(context).colorScheme.primary - : AFThemeExtension.of(context).onBackground, - width: 12, - height: 1, - ), - ); - } -} - -class _RelationCellEditorDatabasePicker extends StatelessWidget { - const _RelationCellEditorDatabasePicker(); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => RelationDatabaseListCubit(), - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(6, 6, 6, 0), - child: FlowyText( - LocaleKeys.grid_relation_noDatabaseSelected.tr(), - maxLines: null, - fontSize: 10, - color: Theme.of(context).hintColor, - ), - ), - Flexible( - child: ListView.separated( - padding: const EdgeInsets.all(6), - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: state.databaseMetas.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final databaseMeta = state.databaseMetas[index]; - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - onTap: () => context.read().add( - RelationCellEvent.selectDatabaseId( - databaseMeta.databaseId, - ), - ), - text: FlowyText( - 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 deleted file mode 100644 index 6ac2a5b807..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart +++ /dev/null @@ -1,535 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.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'; - -const double _editorPanelWidth = 300; - -class SelectOptionCellEditor extends StatefulWidget { - const SelectOptionCellEditor({super.key, required this.cellController}); - - final SelectOptionCellController cellController; - - @override - State createState() => _SelectOptionCellEditorState(); -} - -class _SelectOptionCellEditorState extends State { - final textEditingController = TextEditingController(); - final scrollController = ScrollController(); - final popoverMutex = PopoverMutex(); - late final bloc = SelectOptionCellEditorBloc( - cellController: widget.cellController, - ); - late final FocusNode focusNode; - - @override - void initState() { - super.initState(); - focusNode = FocusNode( - onKeyEvent: (node, event) { - switch (event.logicalKey) { - case LogicalKeyboardKey.arrowUp when event is! KeyUpEvent: - if (textEditingController.value.composing.isCollapsed) { - bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption()); - return KeyEventResult.handled; - } - break; - case LogicalKeyboardKey.arrowDown when event is! KeyUpEvent: - if (textEditingController.value.composing.isCollapsed) { - bloc.add(const SelectOptionCellEditorEvent.focusNextOption()); - return KeyEventResult.handled; - } - break; - case LogicalKeyboardKey.escape when event is! KeyUpEvent: - if (!textEditingController.value.composing.isCollapsed) { - final end = textEditingController.value.composing.end; - final text = textEditingController.text; - - textEditingController.value = TextEditingValue( - text: text, - selection: TextSelection.collapsed(offset: end), - ); - return KeyEventResult.handled; - } - break; - case LogicalKeyboardKey.backspace when event is KeyUpEvent: - if (!textEditingController.text.isNotEmpty) { - bloc.add(const SelectOptionCellEditorEvent.unselectLastOption()); - return KeyEventResult.handled; - } - break; - } - return KeyEventResult.ignored; - }, - ); - } - - @override - void dispose() { - popoverMutex.dispose(); - textEditingController.dispose(); - scrollController.dispose(); - bloc.close(); - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, - child: TextFieldTapRegion( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _TextField( - textEditingController: textEditingController, - scrollController: scrollController, - focusNode: focusNode, - popoverMutex: popoverMutex, - ), - const TypeOptionSeparator(spacing: 0.0), - Flexible( - child: Focus( - descendantsAreFocusable: false, - child: _OptionList( - textEditingController: textEditingController, - popoverMutex: popoverMutex, - ), - ), - ), - ], - ), - ), - ); - } -} - -class _OptionList extends StatelessWidget { - const _OptionList({ - required this.textEditingController, - required this.popoverMutex, - }); - - final TextEditingController textEditingController; - final PopoverMutex popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listenWhen: (prev, curr) => prev.clearFilter != curr.clearFilter, - listener: (context, state) { - if (state.clearFilter) { - textEditingController.clear(); - context - .read() - .add(const SelectOptionCellEditorEvent.resetClearFilterFlag()); - } - }, - buildWhen: (previous, current) => - !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, - ), - ); - }, - header: Padding( - padding: EdgeInsets.only( - bottom: state.createSelectOptionSuggestion != null || - state.options.isNotEmpty - ? 12 - : 0, - ), - child: const _Title(), - ), - footer: state.createSelectOptionSuggestion != null - ? _CreateOptionCell( - suggestion: state.createSelectOptionSuggestion!, - ) - : null, - padding: const EdgeInsets.symmetric(vertical: 8), - ), - ); - } -} - -class _TextField extends StatelessWidget { - const _TextField({ - required this.textEditingController, - required this.scrollController, - required this.focusNode, - required this.popoverMutex, - }); - - final TextEditingController textEditingController; - final ScrollController scrollController; - final FocusNode focusNode; - final PopoverMutex popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final optionMap = LinkedHashMap.fromIterable( - state.selectedOptions, - key: (option) => option.name, - value: (option) => option, - ); - - return Material( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: SelectOptionTextField( - options: state.options, - focusNode: focusNode, - selectedOptionMap: optionMap, - distanceToText: _editorPanelWidth * 0.7, - textController: textEditingController, - scrollController: scrollController, - textSeparators: const [','], - onClick: () => popoverMutex.close(), - newText: (text) => context - .read() - .add(SelectOptionCellEditorEvent.filterOption(text)), - onSubmitted: () { - context - .read() - .add(const SelectOptionCellEditorEvent.submitTextField()); - focusNode.requestFocus(); - }, - onPaste: (tagNames, remainder) { - context.read().add( - SelectOptionCellEditorEvent.selectMultipleOptions( - tagNames, - remainder, - ), - ); - }, - onRemove: (name) => - context.read().add( - SelectOptionCellEditorEvent.unselectOption( - optionMap[name]!.id, - ), - ), - ), - ), - ); - }, - ); - } -} - -class _Title extends StatelessWidget { - const _Title(); - - @override - 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, - ), - ); - } -} - -class _SelectOptionCell extends StatefulWidget { - const _SelectOptionCell({ - super.key, - required this.option, - required this.index, - required this.popoverMutex, - }); - - final SelectOptionPB option; - final int index; - final PopoverMutex popoverMutex; - - @override - State<_SelectOptionCell> createState() => _SelectOptionCellState(); -} - -class _SelectOptionCellState extends State<_SelectOptionCell> { - final _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: _popoverController, - offset: const Offset(8, 0), - margin: EdgeInsets.zero, - asBarrier: true, - 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, - ), - ), - child: Container( - height: 28, - decoration: BoxDecoration( - color: context - .watch() - .state - .focusedOptionId == - widget.option.id - ? AFThemeExtension.of(context).lightGreyHover - : null, - borderRadius: const BorderRadius.all(Radius.circular(6)), - ), - child: SelectOptionTagCell( - option: widget.option, - index: widget.index, - onSelected: _onTap, - children: [ - if (context - .watch() - .state - .selectedOptions - .contains(widget.option)) - FlowyIconButton( - width: 20, - hoverColor: Colors.transparent, - onPressed: _onTap, - icon: FlowySvg( - FlowySvgs.check_s, - color: Theme.of(context).iconTheme.color, - ), - ), - FlowyIconButton( - onPressed: () => _popoverController.show(), - iconPadding: const EdgeInsets.symmetric(horizontal: 6.0), - hoverColor: Colors.transparent, - icon: FlowySvg( - FlowySvgs.three_dots_s, - size: const Size.square(16), - color: AFThemeExtension.of(context).onBackground, - ), - ), - ], - ), - ), - ), - ), - ); - } - - void _onTap() { - widget.popoverMutex.close(); - final bloc = context.read(); - if (bloc.state.selectedOptions.contains(widget.option)) { - bloc.add(SelectOptionCellEditorEvent.unselectOption(widget.option.id)); - } else { - bloc.add(SelectOptionCellEditorEvent.selectOption(widget.option.id)); - } - } -} - -class SelectOptionTagCell extends StatelessWidget { - const SelectOptionTagCell({ - super.key, - required this.option, - required this.onSelected, - this.children = const [], - this.index, - }); - - final SelectOptionPB option; - final VoidCallback onSelected; - final List children; - final int? index; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (index != null) - ReorderableDragStartListener( - index: index!, - child: MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: GestureDetector( - onTap: onSelected, - child: SizedBox( - width: 26, - child: Center( - child: FlowySvg( - FlowySvgs.drag_element_s, - size: const Size.square(14), - color: AFThemeExtension.of(context).onBackground, - ), - ), - ), - ), - ), - ), - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onSelected, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( - alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: SelectOptionTag( - fontSize: 14, - option: option, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - ), - ), - ), - ), - ), - ...children, - ], - ); - } -} - -class _CreateOptionCell extends StatelessWidget { - const _CreateOptionCell({required this.suggestion}); - - final CreateSelectOptionSuggestion suggestion; - - @override - Widget build(BuildContext context) { - return Container( - height: 32, - margin: const EdgeInsets.symmetric(horizontal: 8.0), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: - context.watch().state.focusedOptionId == - createSelectOptionSuggestionId - ? AFThemeExtension.of(context).lightGreyHover - : null, - borderRadius: const BorderRadius.all(Radius.circular(6)), - ), - child: GestureDetector( - onTap: () => context - .read() - .add(const SelectOptionCellEditorEvent.createOption()), - child: MouseRegion( - onEnter: (_) { - context.read().add( - const SelectOptionCellEditorEvent.updateFocusedOption( - createSelectOptionSuggestionId, - ), - ); - }, - child: Row( - children: [ - FlowyText( - LocaleKeys.grid_selectOption_create.tr(), - color: Theme.of(context).hintColor, - ), - const HSpace(10), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: SelectOptionTag( - name: suggestion.name, - color: suggestion.color.toColor(context), - fontSize: 14, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index a03167df9d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'dart:collection'; - -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -import 'extension.dart'; - -class SelectOptionTextField extends StatefulWidget { - const SelectOptionTextField({ - super.key, - required this.options, - required this.selectedOptionMap, - required this.distanceToText, - required this.textSeparators, - required this.textController, - required this.focusNode, - required this.onSubmitted, - required this.newText, - required this.onPaste, - required this.onRemove, - this.scrollController, - this.onClick, - }); - - final List options; - final LinkedHashMap selectedOptionMap; - final double distanceToText; - final List textSeparators; - final TextEditingController textController; - final ScrollController? scrollController; - final FocusNode focusNode; - - final Function() onSubmitted; - final Function(String) newText; - final Function(List, String) onPaste; - final Function(String) onRemove; - final VoidCallback? onClick; - - @override - State createState() => _SelectOptionTextFieldState(); -} - -class _SelectOptionTextFieldState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.focusNode.requestFocus(); - _scrollToEnd(); - }); - widget.textController.addListener(_onChanged); - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (oldWidget.selectedOptionMap.length < widget.selectedOptionMap.length) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToEnd(); - }); - } - - if (oldWidget.textController != widget.textController) { - oldWidget.textController.removeListener(_onChanged); - widget.textController.addListener(_onChanged); - } - - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - widget.textController.removeListener(_onChanged); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: widget.textController, - focusNode: widget.focusNode, - onTap: widget.onClick, - onSubmitted: (_) => widget.onSubmitted(), - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.outline), - borderRadius: Corners.s10Border, - ), - isDense: true, - prefixIcon: _renderTags(context), - prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), - borderRadius: Corners.s10Border, - ), - ), - ); - } - - void _scrollToEnd() { - if (widget.scrollController?.hasClients ?? false) { - widget.scrollController?.animateTo( - widget.scrollController!.position.maxScrollExtent, - duration: const Duration(milliseconds: 150), - curve: Curves.easeOut, - ); - } - } - - void _onChanged() { - if (!widget.textController.value.composing.isCollapsed) { - return; - } - - // split input - final (submitted, remainder) = splitInput( - widget.textController.text.trimLeft(), - widget.textSeparators, - ); - - if (submitted.isNotEmpty) { - widget.textController.text = remainder; - widget.textController.selection = - TextSelection.collapsed(offset: widget.textController.text.length); - } - widget.onPaste(submitted, remainder); - } - - Widget? _renderTags(BuildContext context) { - if (widget.selectedOptionMap.isEmpty) { - return null; - } - - final children = widget.selectedOptionMap.values - .map( - (option) => SelectOptionTag( - option: option, - onRemove: (option) => widget.onRemove(option), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - ), - ) - .toList(); - - return Focus( - descendantsAreFocusable: false, - child: MouseRegion( - cursor: SystemMouseCursors.basic, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.trackpad, - PointerDeviceKind.stylus, - PointerDeviceKind.invertedStylus, - }, - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: widget.scrollController, - child: Wrap(spacing: 4, children: children), - ), - ), - ), - ), - ); - } -} - -@visibleForTesting -(List, String) splitInput(String input, List textSeparators) { - final List splits = []; - String currentString = ''; - - // split the string into tokens - for (final char in input.split('')) { - if (textSeparators.contains(char)) { - if (currentString.trim().isNotEmpty) { - splits.add(currentString.trim()); - } - currentString = ''; - continue; - } - currentString += char; - } - // add the remainder (might be '') - splits.add(currentString); - - final submittedOptions = splits.sublist(0, splits.length - 1).toList(); - final remainder = splits.elementAt(splits.length - 1).trimLeft(); - - return (submittedOptions, remainder); -} 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 deleted file mode 100644 index f8118a7e51..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension DatabaseLayoutExtension on DatabaseLayoutPB { - String get layoutName { - return switch (this) { - DatabaseLayoutPB.Board => LocaleKeys.board_menuName.tr(), - DatabaseLayoutPB.Calendar => LocaleKeys.calendar_menuName.tr(), - DatabaseLayoutPB.Grid => LocaleKeys.grid_menuName.tr(), - _ => "", - }; - } - - ViewLayoutPB get layoutType { - return switch (this) { - DatabaseLayoutPB.Board => ViewLayoutPB.Board, - DatabaseLayoutPB.Calendar => ViewLayoutPB.Calendar, - DatabaseLayoutPB.Grid => ViewLayoutPB.Grid, - _ => throw UnimplementedError(), - }; - } - - FlowySvgData get icon { - return switch (this) { - DatabaseLayoutPB.Board => FlowySvgs.board_s, - DatabaseLayoutPB.Calendar => FlowySvgs.calendar_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 deleted file mode 100644 index a218e1ed68..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.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(); -} - -class _DatabaseViewWidgetState extends State { - /// Listens to the view updates. - late final ViewListener _listener; - - /// Notifies the view layout type changes. When the layout type changes, - /// the widget of the view will be updated. - late final ValueNotifier _layoutTypeChangeNotifier; - - /// The view will be updated by the [ViewListener]. - late ViewPB view; - - late Plugin viewPlugin; - - @override - void initState() { - super.initState(); - view = widget.view; - viewPlugin = view.plugin()..init(); - _listenOnViewUpdated(); - } - - @override - void dispose() { - _layoutTypeChangeNotifier.dispose(); - _listener.stop(); - viewPlugin.dispose(); - super.dispose(); - } - - @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, - }, - ), - ); - } - - void _listenOnViewUpdated() { - _listener = ViewListener(viewId: widget.view.id) - ..start( - onViewUpdated: (updatedView) { - if (mounted) { - view = updatedView; - _layoutTypeChangeNotifier.value = view.layout; - } - }, - ); - - _layoutTypeChangeNotifier = ValueNotifier(widget.view.layout); - } -} 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 deleted file mode 100644 index 88cd88ee68..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart +++ /dev/null @@ -1,778 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -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/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/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_backend/protobuf/flowy-database2/protobuf.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'; - -enum FieldEditorPage { - general, - details, -} - -class FieldEditor extends StatefulWidget { - const FieldEditor({ - super.key, - required this.viewId, - required this.fieldInfo, - required this.fieldController, - required this.isNewField, - this.initialPage = FieldEditorPage.details, - this.onFieldInserted, - }); - - final String viewId; - final FieldInfo fieldInfo; - 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); - - @override - void initState() { - super.initState(); - _currentPage = widget.initialPage; - } - - @override - void dispose() { - popoverMutex.dispose(); - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => FieldEditorBloc( - viewId: widget.viewId, - fieldInfo: widget.fieldInfo, - fieldController: widget.fieldController, - onFieldInserted: widget.onFieldInserted, - isNew: widget.isNewField, - ), - child: _currentPage == FieldEditorPage.general - ? _fieldGeneral() - : _fieldDetails(), - ); - } - - Widget _fieldGeneral() { - return SizedBox( - width: 240, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _NameAndIcon( - popoverMutex: popoverMutex, - textController: textController, - padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), - ), - VSpace(GridSize.typeOptionSeparatorHeight), - _EditFieldButton( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - onTap: () { - setState(() => _currentPage = FieldEditorPage.details); - }, - ), - VSpace(GridSize.typeOptionSeparatorHeight), - _actionCell(FieldAction.insertLeft), - VSpace(GridSize.typeOptionSeparatorHeight), - _actionCell(FieldAction.insertRight), - VSpace(GridSize.typeOptionSeparatorHeight), - _actionCell(FieldAction.toggleVisibility), - VSpace(GridSize.typeOptionSeparatorHeight), - _actionCell(FieldAction.duplicate), - VSpace(GridSize.typeOptionSeparatorHeight), - _actionCell(FieldAction.clearData), - VSpace(GridSize.typeOptionSeparatorHeight), - _actionCell(FieldAction.delete), - const TypeOptionSeparator(spacing: 8.0), - _actionCell(FieldAction.wrap), - const VSpace(8.0), - ], - ), - ); - } - - 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, - child: FieldDetailsEditor( - viewId: widget.viewId, - textEditingController: textController, - ), - ); - } -} - -class _EditFieldButton extends StatelessWidget { - const _EditFieldButton({ - required this.padding, - this.onTap, - }); - - final EdgeInsetsGeometry padding; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - return Container( - height: GridSize.popoverItemHeight, - padding: padding, - child: FlowyButton( - leftIcon: const FlowySvg(FlowySvgs.edit_s), - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.grid_field_editProperty.tr(), - ), - onTap: onTap, - ), - ); - } -} - -class FieldActionCell extends StatelessWidget { - const FieldActionCell({ - super.key, - required this.viewId, - required this.fieldInfo, - required this.action, - this.popoverMutex, - }); - - final String viewId; - final FieldInfo fieldInfo; - final FieldAction action; - final PopoverMutex? popoverMutex; - - @override - Widget build(BuildContext context) { - bool enable = true; - // If the field is primary, delete and duplicate are disabled. - if (fieldInfo.isPrimary && - (action == FieldAction.duplicate || action == FieldAction.delete)) { - enable = false; - } - return FlowyIconTextButton( - resetHoverOnRebuild: false, - disable: !enable, - 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( - fieldInfo, - enable - ? action == FieldAction.delete && onHover - ? Theme.of(context).colorScheme.error - : null - : Theme.of(context).disabledColor, - ), - rightIconBuilder: (_) => action.trailing(context, fieldInfo), - ); - } -} - -enum FieldAction { - insertLeft, - insertRight, - toggleVisibility, - duplicate, - clearData, - delete, - wrap; - - Widget? leading(FieldInfo fieldInfo, Color? color) { - FlowySvgData? svgData; - switch (this) { - case FieldAction.insertLeft: - svgData = FlowySvgs.arrow_s; - case FieldAction.insertRight: - svgData = FlowySvgs.arrow_s; - case FieldAction.toggleVisibility: - if (fieldInfo.visibility != null && - fieldInfo.visibility == FieldVisibility.AlwaysHidden) { - svgData = FlowySvgs.show_m; - } else { - svgData = FlowySvgs.hide_s; - } - case FieldAction.duplicate: - svgData = FlowySvgs.copy_s; - case FieldAction.clearData: - svgData = FlowySvgs.reload_s; - case FieldAction.delete: - svgData = FlowySvgs.delete_s; - default: - } - - if (svgData == null) { - return null; - } - final icon = FlowySvg( - svgData, - size: const Size.square(16), - color: color, - ); - return this == FieldAction.insertRight - ? Transform.flip(flipX: true, child: icon) - : icon; - } - - Widget? trailing(BuildContext context, FieldInfo fieldInfo) { - if (this == FieldAction.wrap) { - return Toggle( - value: fieldInfo.wrapCellContent ?? false, - onChanged: (_) => context - .read() - .add(const FieldEditorEvent.toggleWrapCellContent()), - padding: EdgeInsets.zero, - ); - } - - return null; - } - - String title(FieldInfo fieldInfo) { - switch (this) { - case FieldAction.insertLeft: - return LocaleKeys.grid_field_insertLeft.tr(); - case FieldAction.insertRight: - return LocaleKeys.grid_field_insertRight.tr(); - case FieldAction.toggleVisibility: - if (fieldInfo.visibility != null && - fieldInfo.visibility == FieldVisibility.AlwaysHidden) { - return LocaleKeys.grid_field_show.tr(); - } else { - return LocaleKeys.grid_field_hide.tr(); - } - case FieldAction.duplicate: - return LocaleKeys.grid_field_duplicate.tr(); - case FieldAction.clearData: - return LocaleKeys.grid_field_clear.tr(); - case FieldAction.delete: - return LocaleKeys.grid_field_delete.tr(); - case FieldAction.wrap: - return LocaleKeys.grid_field_wrapCellContent.tr(); - } - } - - void run(BuildContext context, String viewId, FieldInfo fieldInfo) { - switch (this) { - case FieldAction.insertLeft: - PopoverContainer.of(context).close(); - context - .read() - .add(const FieldEditorEvent.insertLeft()); - break; - case FieldAction.insertRight: - PopoverContainer.of(context).close(); - context - .read() - .add(const FieldEditorEvent.insertRight()); - break; - case FieldAction.toggleVisibility: - PopoverContainer.of(context).close(); - context - .read() - .add(const FieldEditorEvent.toggleFieldVisibility()); - break; - case FieldAction.duplicate: - PopoverContainer.of(context).close(); - FieldBackendService.duplicateField( - viewId: viewId, - fieldId: fieldInfo.id, - ); - 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: () { - FieldBackendService.clearField( - viewId: viewId, - fieldId: fieldInfo.id, - ); - }, - ); - break; - case FieldAction.delete: - PopoverContainer.of(context).closeAll(); - showConfirmDeletionDialog( - context: context, - name: LocaleKeys.grid_field_label.tr(), - description: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), - onConfirm: () { - FieldBackendService.deleteField( - viewId: viewId, - fieldId: fieldInfo.id, - ); - }, - ); - break; - case FieldAction.wrap: - context - .read() - .add(const FieldEditorEvent.toggleWrapCellContent()); - break; - } - } -} - -class FieldDetailsEditor extends StatefulWidget { - const FieldDetailsEditor({ - super.key, - required this.viewId, - required this.textEditingController, - this.onAction, - }); - - final String viewId; - final TextEditingController textEditingController; - final Function()? onAction; - - @override - State createState() => _FieldDetailsEditorState(); -} - -class _FieldDetailsEditorState extends State { - final PopoverMutex popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final List children = [ - _NameAndIcon( - popoverMutex: popoverMutex, - textController: widget.textEditingController, - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - ), - const VSpace(8.0), - SwitchFieldButton(popoverMutex: popoverMutex), - const TypeOptionSeparator(spacing: 8.0), - Flexible( - child: FieldTypeOptionEditor( - viewId: widget.viewId, - popoverMutex: popoverMutex, - ), - ), - _addFieldVisibilityToggleButton(), - _addDuplicateFieldButton(), - _addDeleteFieldButton(), - ]; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ); - } - - Widget _addFieldVisibilityToggleButton() { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FieldActionCell( - viewId: widget.viewId, - fieldInfo: state.field, - action: FieldAction.toggleVisibility, - popoverMutex: popoverMutex, - ), - ); - }, - ); - } - - Widget _addDeleteFieldButton() { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0), - child: FieldActionCell( - viewId: widget.viewId, - fieldInfo: state.field, - action: FieldAction.delete, - popoverMutex: popoverMutex, - ), - ); - }, - ); - } - - Widget _addDuplicateFieldButton() { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0), - child: FieldActionCell( - viewId: widget.viewId, - fieldInfo: state.field, - action: FieldAction.duplicate, - popoverMutex: popoverMutex, - ), - ); - }, - ); - } -} - -class FieldTypeOptionEditor extends StatelessWidget { - const FieldTypeOptionEditor({ - super.key, - required this.viewId, - required this.popoverMutex, - }); - - final String viewId; - final PopoverMutex popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.field.isPrimary) { - return const SizedBox.shrink(); - } - final typeOptionEditor = makeTypeOptionEditor( - context: context, - viewId: viewId, - field: state.field.field, - popoverMutex: popoverMutex, - onTypeOptionUpdated: (Uint8List typeOptionData) { - context - .read() - .add(FieldEditorEvent.updateTypeOption(typeOptionData)); - }, - ); - - if (typeOptionEditor == null) { - return const SizedBox.shrink(); - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible(child: typeOptionEditor), - const TypeOptionSeparator(spacing: 8.0), - ], - ); - }, - ); - } -} - -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, - this.popoverMutex, - }); - - final TextEditingController textController; - final PopoverMutex? popoverMutex; - - @override - State createState() => _FieldNameTextFieldState(); -} - -class _FieldNameTextFieldState extends State { - 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(); - } - - @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)); - }, - ); - } - - void _onFocusChanged() { - if (focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - } - - void _onPopoverChanged() { - if (focusNode.hasFocus) { - focusNode.unfocus(); - } - } -} - -class SwitchFieldButton extends StatefulWidget { - const SwitchFieldButton({ - super.key, - required this.popoverMutex, - }); - - final PopoverMutex popoverMutex; - - @override - State createState() => _SwitchFieldButtonState(); -} - -class _SwitchFieldButtonState extends State { - final PopoverController _popoverController = PopoverController(); - - @override - 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, - ), - ), - ), - ), - ); - } - return SizedBox( - height: GridSize.popoverItemHeight, - child: AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(460, 540)), - triggerActions: PopoverTriggerFlags.hover, - mutex: widget.popoverMutex, - controller: _popoverController, - offset: const Offset(8, 0), - margin: const EdgeInsets.all(8), - popupBuilder: (BuildContext popoverContext) { - return FieldTypeList( - onSelectField: (newFieldType) { - context - .read() - .add(FieldEditorEvent.switchFieldType(newFieldType)); - }, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FlowyButton( - onTap: () => _popoverController.show(), - text: FlowyText( - state.field.fieldType.i18n, - lineHeight: 1.0, - ), - leftIcon: FlowySvg( - state.field.fieldType.svgData, - ), - rightIcon: const FlowySvg( - FlowySvgs.more_s, - ), - ), - ), - ), - ); - }, - ); - } -} 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 deleted file mode 100644 index 6661d5cd2d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; - -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:flowy_infra_ui/flowy_infra_ui.dart'; - -typedef SelectFieldCallback = void Function(FieldType); - -const List _supportedFieldTypes = [ - FieldType.RichText, - FieldType.Number, - FieldType.SingleSelect, - FieldType.MultiSelect, - FieldType.DateTime, - FieldType.Media, - FieldType.URL, - FieldType.Checkbox, - FieldType.Checklist, - FieldType.LastEditedTime, - FieldType.CreatedTime, - FieldType.Relation, - FieldType.Summary, - FieldType.Translate, - // FieldType.Time, -]; - -class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { - const FieldTypeList({required this.onSelectField, super.key}); - - final SelectFieldCallback onSelectField; - - @override - Widget build(BuildContext context) { - final cells = _supportedFieldTypes.map((fieldType) { - return FieldTypeCell( - fieldType: fieldType, - onSelectField: (fieldType) { - onSelectField(fieldType); - PopoverContainer.of(context).closeAll(); - }, - ); - }).toList(); - - return SizedBox( - width: 140, - child: ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, - ), - ); - } -} - -class FieldTypeCell extends StatelessWidget { - const FieldTypeCell({ - super.key, - required this.fieldType, - required this.onSelectField, - }); - - final FieldType fieldType; - final SelectFieldCallback onSelectField; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText(fieldType.i18n, lineHeight: 1.0), - 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 deleted file mode 100644 index 624f9f1fb2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart +++ /dev/null @@ -1,71 +0,0 @@ -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'; - -import 'checkbox.dart'; -import 'checklist.dart'; -import 'date.dart'; -import 'multi_select.dart'; -import 'number.dart'; -import 'relation.dart'; -import 'rich_text.dart'; -import 'single_select.dart'; -import 'summary.dart'; -import 'time.dart'; -import 'timestamp.dart'; -import 'url.dart'; - -typedef TypeOptionDataCallback = void Function(Uint8List typeOptionData); - -abstract class TypeOptionEditorFactory { - factory TypeOptionEditorFactory.makeBuilder(FieldType fieldType) { - return switch (fieldType) { - FieldType.RichText => const RichTextTypeOptionEditorFactory(), - FieldType.Number => const NumberTypeOptionEditorFactory(), - FieldType.URL => const URLTypeOptionEditorFactory(), - FieldType.DateTime => const DateTypeOptionEditorFactory(), - FieldType.LastEditedTime => const TimestampTypeOptionEditorFactory(), - FieldType.CreatedTime => const TimestampTypeOptionEditorFactory(), - FieldType.SingleSelect => const SingleSelectTypeOptionEditorFactory(), - FieldType.MultiSelect => const MultiSelectTypeOptionEditorFactory(), - FieldType.Checkbox => const CheckboxTypeOptionEditorFactory(), - FieldType.Checklist => const ChecklistTypeOptionEditorFactory(), - FieldType.Relation => const RelationTypeOptionEditorFactory(), - FieldType.Summary => const SummaryTypeOptionEditorFactory(), - FieldType.Time => const TimeTypeOptionEditorFactory(), - FieldType.Translate => const TranslateTypeOptionEditorFactory(), - FieldType.Media => const MediaTypeOptionEditorFactory(), - _ => throw UnimplementedError(), - }; - } - - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }); -} - -Widget? makeTypeOptionEditor({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, -}) { - final editorBuilder = TypeOptionEditorFactory.makeBuilder(field.fieldType); - return editorBuilder.build( - context: context, - viewId: viewId, - field: field, - onTypeOptionUpdated: onTypeOptionUpdated, - popoverMutex: popoverMutex, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checkbox.dart deleted file mode 100644 index 70701fb290..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checkbox.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; - -import 'builder.dart'; - -class CheckboxTypeOptionEditorFactory implements TypeOptionEditorFactory { - const CheckboxTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) => - null; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checklist.dart deleted file mode 100644 index 0e045cfe56..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checklist.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; - -import 'builder.dart'; - -class ChecklistTypeOptionEditorFactory implements TypeOptionEditorFactory { - const ChecklistTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) => - null; -} 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 deleted file mode 100644 index ab216c7b98..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart +++ /dev/null @@ -1,120 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:protobuf/protobuf.dart'; - -import '../../../grid/presentation/layout/sizes.dart'; -import 'builder.dart'; -import 'date/date_time_format.dart'; - -class DateTypeOptionEditorFactory implements TypeOptionEditorFactory { - const DateTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _renderDateFormatButton( - typeOption, - popoverMutex, - onTypeOptionUpdated, - ), - VSpace(GridSize.typeOptionSeparatorHeight), - _renderTimeFormatButton( - typeOption, - popoverMutex, - onTypeOptionUpdated, - ), - ], - ); - } - - Widget _renderDateFormatButton( - DateTypeOptionPB typeOption, - PopoverMutex popoverMutex, - TypeOptionDataCallback onTypeOptionUpdated, - ) { - return AppFlowyPopover( - mutex: popoverMutex, - asBarrier: true, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - constraints: BoxConstraints.loose(const Size(460, 440)), - popupBuilder: (popoverContext) { - return DateFormatList( - selectedFormat: typeOption.dateFormat, - onSelected: (format) { - final newTypeOption = - _updateTypeOption(typeOption: typeOption, dateFormat: format); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(popoverContext).close(); - }, - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: DateFormatButton(), - ), - ); - } - - Widget _renderTimeFormatButton( - DateTypeOptionPB typeOption, - PopoverMutex popoverMutex, - TypeOptionDataCallback onTypeOptionUpdated, - ) { - return AppFlowyPopover( - mutex: popoverMutex, - asBarrier: true, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - constraints: BoxConstraints.loose(const Size(460, 440)), - popupBuilder: (BuildContext popoverContext) { - return TimeFormatList( - selectedFormat: typeOption.timeFormat, - onSelected: (format) { - final newTypeOption = - _updateTypeOption(typeOption: typeOption, timeFormat: format); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(popoverContext).close(); - }, - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: TimeFormatButton(), - ), - ); - } - - DateTypeOptionPB _parseTypeOptionData(List data) { - return DateTypeOptionDataParser().fromBuffer(data); - } - - DateTypeOptionPB _updateTypeOption({ - required DateTypeOptionPB typeOption, - DateFormatPB? dateFormat, - TimeFormatPB? timeFormat, - }) { - typeOption.freeze(); - return typeOption.rebuild((typeOption) { - if (dateFormat != null) { - typeOption.dateFormat = dateFormat; - } - - if (timeFormat != null) { - typeOption.timeFormat = timeFormat; - } - }); - } -} 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 deleted file mode 100644 index 862e46fc3b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart +++ /dev/null @@ -1,274 +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:appflowy/plugins/database/grid/presentation/layout/sizes.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:flowy_infra_ui/flowy_infra_ui.dart'; - -class DateFormatButton extends StatelessWidget { - const DateFormatButton({ - super.key, - this.onTap, - this.onHover, - }); - - final VoidCallback? onTap; - final void Function(bool)? onHover; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - LocaleKeys.grid_field_dateFormat.tr(), - lineHeight: 1.0, - ), - onTap: onTap, - onHover: onHover, - rightIcon: const FlowySvg(FlowySvgs.more_s), - ), - ); - } -} - -class TimeFormatButton extends StatelessWidget { - const TimeFormatButton({ - super.key, - this.onTap, - this.onHover, - }); - - final VoidCallback? onTap; - final void Function(bool)? onHover; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - LocaleKeys.grid_field_timeFormat.tr(), - lineHeight: 1.0, - ), - onTap: onTap, - onHover: onHover, - rightIcon: const FlowySvg(FlowySvgs.more_s), - ), - ); - } -} - -class DateFormatList extends StatelessWidget { - const DateFormatList({ - super.key, - required this.selectedFormat, - required this.onSelected, - }); - - final DateFormatPB selectedFormat; - final Function(DateFormatPB format) onSelected; - - @override - Widget build(BuildContext context) { - final cells = DateFormatPB.values - .where((value) => value != DateFormatPB.FriendlyFull) - .map((format) { - return DateFormatCell( - dateFormat: format, - onSelected: onSelected, - isSelected: selectedFormat == format, - ); - }).toList(); - - return SizedBox( - width: 180, - child: ListView.separated( - shrinkWrap: true, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - itemCount: cells.length, - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, - ), - ); - } -} - -class DateFormatCell extends StatelessWidget { - const DateFormatCell({ - super.key, - required this.dateFormat, - required this.onSelected, - required this.isSelected, - }); - - final DateFormatPB dateFormat; - final Function(DateFormatPB format) onSelected; - final bool isSelected; - - @override - Widget build(BuildContext context) { - Widget? checkmark; - if (isSelected) { - checkmark = const FlowySvg(FlowySvgs.check_s); - } - - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - dateFormat.title(), - lineHeight: 1.0, - ), - rightIcon: checkmark, - onTap: () => onSelected(dateFormat), - ), - ); - } -} - -extension DateFormatExtension on DateFormatPB { - String title() { - switch (this) { - case DateFormatPB.Friendly: - return LocaleKeys.grid_field_dateFormatFriendly.tr(); - case DateFormatPB.ISO: - return LocaleKeys.grid_field_dateFormatISO.tr(); - case DateFormatPB.Local: - return LocaleKeys.grid_field_dateFormatLocal.tr(); - case DateFormatPB.US: - 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; - } - } -} - -class TimeFormatList extends StatelessWidget { - const TimeFormatList({ - super.key, - required this.selectedFormat, - required this.onSelected, - }); - - final TimeFormatPB selectedFormat; - final Function(TimeFormatPB format) onSelected; - - @override - Widget build(BuildContext context) { - final cells = TimeFormatPB.values.map((format) { - return TimeFormatCell( - isSelected: format == selectedFormat, - timeFormat: format, - onSelected: onSelected, - ); - }).toList(); - - return SizedBox( - width: 120, - child: ListView.separated( - shrinkWrap: true, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - itemCount: cells.length, - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, - ), - ); - } -} - -class TimeFormatCell extends StatelessWidget { - const TimeFormatCell({ - super.key, - required this.timeFormat, - required this.onSelected, - required this.isSelected, - }); - - final TimeFormatPB timeFormat; - final bool isSelected; - final Function(TimeFormatPB format) onSelected; - - @override - Widget build(BuildContext context) { - Widget? checkmark; - if (isSelected) { - checkmark = const FlowySvg(FlowySvgs.check_s); - } - - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - timeFormat.title(), - lineHeight: 1.0, - ), - rightIcon: checkmark, - onTap: () => onSelected(timeFormat), - ), - ); - } -} - -extension TimeFormatExtension on TimeFormatPB { - String title() { - switch (this) { - case TimeFormatPB.TwelveHour: - return LocaleKeys.grid_field_timeFormatTwelveHour.tr(); - case TimeFormatPB.TwentyFourHour: - return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr(); - default: - throw UnimplementedError; - } - } -} - -class IncludeTimeButton extends StatelessWidget { - const IncludeTimeButton({ - super.key, - required this.onChanged, - required this.includeTime, - }); - - final Function(bool value) onChanged; - final bool includeTime; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: GridSize.typeOptionContentInsets, - child: Row( - children: [ - FlowySvg( - FlowySvgs.clock_alarm_s, - color: Theme.of(context).iconTheme.color, - ), - const HSpace(6), - FlowyText(LocaleKeys.grid_field_includeTime.tr()), - const Spacer(), - Toggle( - value: includeTime, - onChanged: onChanged, - 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/multi_select.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/multi_select.dart deleted file mode 100644 index 1e1de37ece..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/multi_select.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.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/material.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; - -import 'builder.dart'; -import 'select/select_option.dart'; - -class MultiSelectTypeOptionEditorFactory implements TypeOptionEditorFactory { - const MultiSelectTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - return SelectOptionTypeOptionWidget( - options: typeOption.options, - beginEdit: () => PopoverContainer.of(context).closeAll(), - popoverMutex: popoverMutex, - typeOptionAction: MultiSelectAction( - viewId: viewId, - fieldId: field.id, - onTypeOptionUpdated: onTypeOptionUpdated, - ), - ); - } - - MultiSelectTypeOptionPB _parseTypeOptionData(List data) { - return MultiSelectTypeOptionDataParser().fromBuffer(data); - } -} 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 deleted file mode 100644 index b8a40907c6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -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: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:protobuf/protobuf.dart'; - -import '../../../grid/presentation/layout/sizes.dart'; -import '../../../grid/presentation/widgets/common/type_option_separator.dart'; -import 'builder.dart'; - -class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory { - const NumberTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - final selectNumUnitButton = SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - rightIcon: const FlowySvg(FlowySvgs.more_s), - text: FlowyText( - lineHeight: 1.0, - typeOption.format.title(), - ), - ), - ); - - final numFormatTitle = Container( - padding: const EdgeInsets.only(left: 6), - height: GridSize.popoverItemHeight, - alignment: Alignment.centerLeft, - child: FlowyText.regular( - LocaleKeys.grid_field_numberFormat.tr(), - color: Theme.of(context).hintColor, - fontSize: 11, - ), - ); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - numFormatTitle, - AppFlowyPopover( - mutex: popoverMutex, - triggerActions: - PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(16, 0), - constraints: BoxConstraints.loose(const Size(460, 440)), - margin: EdgeInsets.zero, - child: selectNumUnitButton, - popupBuilder: (BuildContext popoverContext) { - return NumberFormatList( - selectedFormat: typeOption.format, - onSelected: (format) { - final newTypeOption = _updateNumberFormat(typeOption, format); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(popoverContext).close(); - }, - ); - }, - ), - ], - ), - ); - } - - NumberTypeOptionPB _parseTypeOptionData(List data) { - return NumberTypeOptionDataParser().fromBuffer(data); - } - - NumberTypeOptionPB _updateNumberFormat( - NumberTypeOptionPB typeOption, - NumberFormatPB format, - ) { - typeOption.freeze(); - return typeOption.rebuild((typeOption) => typeOption.format = format); - } -} - -typedef SelectNumberFormatCallback = void Function(NumberFormatPB format); - -class NumberFormatList extends StatelessWidget { - const NumberFormatList({ - super.key, - required this.selectedFormat, - required this.onSelected, - }); - - final NumberFormatPB selectedFormat; - final SelectNumberFormatCallback onSelected; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => NumberFormatBloc(), - child: SizedBox( - width: 180, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const _FilterTextField(), - const TypeOptionSeparator(spacing: 0.0), - BlocBuilder( - builder: (context, state) { - final cells = state.formats.map((format) { - return NumberFormatCell( - isSelected: format == selectedFormat, - format: format, - onSelected: (format) { - onSelected(format); - }, - ); - }).toList(); - - final list = ListView.separated( - shrinkWrap: true, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - itemCount: cells.length, - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, - padding: const EdgeInsets.all(6.0), - ); - return Flexible(child: list); - }, - ), - ], - ), - ), - ); - } -} - -class NumberFormatCell extends StatelessWidget { - const NumberFormatCell({ - super.key, - required this.format, - required this.isSelected, - required this.onSelected, - }); - - final NumberFormatPB format; - final bool isSelected; - final SelectNumberFormatCallback onSelected; - - @override - Widget build(BuildContext context) { - final checkmark = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - format.title(), - lineHeight: 1.0, - ), - onTap: () => onSelected(format), - rightIcon: checkmark, - ), - ); - } -} - -class _FilterTextField extends StatelessWidget { - const _FilterTextField(); - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(6.0), - child: FlowyTextField( - onChanged: (text) => context - .read() - .add(NumberFormatEvent.setFilter(text)), - ), - ); - } -} 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 deleted file mode 100644 index 2ee3222b23..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -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/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: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:protobuf/protobuf.dart'; - -import 'builder.dart'; - -class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory { - const RelationTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - return BlocProvider( - create: (_) => RelationDatabaseListCubit(), - child: Builder( - builder: (context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.only(left: 14, right: 8), - height: GridSize.popoverItemHeight, - alignment: Alignment.centerLeft, - child: FlowyText.regular( - LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(), - color: Theme.of(context).hintColor, - fontSize: 11, - ), - ), - AppFlowyPopover( - mutex: popoverMutex, - triggerActions: - PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(6, 0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: BlocBuilder( - builder: (context, state) { - final databaseMeta = - state.databaseMetas.firstWhereOrNull( - (meta) => meta.databaseId == typeOption.databaseId, - ); - return FlowyText( - lineHeight: 1.0, - databaseMeta == null - ? LocaleKeys - .grid_relation_relatedDatabasePlaceholder - .tr() - : databaseMeta.databaseName, - color: databaseMeta == null - ? Theme.of(context).hintColor - : null, - overflow: TextOverflow.ellipsis, - ); - }, - ), - rightIcon: const FlowySvg(FlowySvgs.more_s), - ), - ), - popupBuilder: (popoverContext) { - return BlocProvider.value( - value: context.read(), - child: _DatabaseList( - onSelectDatabase: (newDatabaseId) { - final newTypeOption = _updateTypeOption( - typeOption: typeOption, - databaseId: newDatabaseId, - ); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(context).close(); - }, - currentDatabaseId: typeOption.databaseId, - ), - ); - }, - ), - ], - ); - }, - ), - ); - } - - RelationTypeOptionPB _parseTypeOptionData(List data) { - return RelationTypeOptionDataParser().fromBuffer(data); - } - - RelationTypeOptionPB _updateTypeOption({ - required RelationTypeOptionPB typeOption, - required String databaseId, - }) { - typeOption.freeze(); - return typeOption.rebuild((typeOption) { - typeOption.databaseId = databaseId; - }); - } -} - -class _DatabaseList extends StatelessWidget { - const _DatabaseList({ - required this.onSelectDatabase, - required this.currentDatabaseId, - }); - - final String currentDatabaseId; - final void Function(String databaseId) onSelectDatabase; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final children = state.databaseMetas.map((meta) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - onTap: () => onSelectDatabase(meta.databaseId), - text: FlowyText( - lineHeight: 1.0, - meta.databaseName, - overflow: TextOverflow.ellipsis, - ), - rightIcon: meta.databaseId == currentDatabaseId - ? const FlowySvg( - FlowySvgs.check_s, - ) - : null, - ), - ); - }).toList(); - - return ListView.separated( - shrinkWrap: true, - padding: EdgeInsets.zero, - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: children.length, - itemBuilder: (context, index) => children[index], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/rich_text.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/rich_text.dart deleted file mode 100644 index f13a4515fb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/rich_text.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; -import 'builder.dart'; - -class RichTextTypeOptionEditorFactory implements TypeOptionEditorFactory { - const RichTextTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) => - null; -} 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 deleted file mode 100644 index 5201630cc7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart +++ /dev/null @@ -1,324 +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/database/application/field/type_option/select_option_type_option_bloc.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart'; -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: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 'select_option_editor.dart'; - -class SelectOptionTypeOptionWidget extends StatelessWidget { - const SelectOptionTypeOptionWidget({ - super.key, - required this.options, - required this.beginEdit, - required this.typeOptionAction, - this.popoverMutex, - }); - - final List options; - final VoidCallback beginEdit; - final ISelectOptionAction typeOptionAction; - final PopoverMutex? popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SelectOptionTypeOptionBloc( - options: options, - typeOptionAction: typeOptionAction, - ), - child: - BlocBuilder( - builder: (context, state) { - final List children = [ - const _OptionTitle(), - const VSpace(4), - if (state.isEditingOption) ...[ - CreateOptionTextField(popoverMutex: popoverMutex), - const VSpace(4), - ] else - const _AddOptionButton(), - const VSpace(4), - Flexible( - child: _OptionList( - popoverMutex: popoverMutex, - ), - ), - ]; - - return Column( - mainAxisSize: MainAxisSize.min, - children: children, - ); - }, - ), - ); - } -} - -class _OptionTitle extends StatelessWidget { - const _OptionTitle(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: FlowyText.regular( - LocaleKeys.grid_field_optionTitle.tr(), - fontSize: 11, - color: Theme.of(context).hintColor, - ), - ), - ); - }, - ); - } -} - -class _OptionCell extends StatefulWidget { - const _OptionCell({ - super.key, - required this.option, - required this.index, - this.popoverMutex, - }); - - final SelectOptionPB option; - final int index; - final PopoverMutex? popoverMutex; - - @override - State<_OptionCell> createState() => _OptionCellState(); -} - -class _OptionCellState extends State<_OptionCell> { - final PopoverController _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - final child = SizedBox( - height: 28, - child: SelectOptionTagCell( - option: widget.option, - index: widget.index, - onSelected: () => _popoverController.show(), - children: [ - FlowyIconButton( - onPressed: () => _popoverController.show(), - iconPadding: const EdgeInsets.symmetric(horizontal: 6.0), - hoverColor: Colors.transparent, - icon: FlowySvg( - FlowySvgs.three_dots_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(16), - ), - ), - ], - ), - ); - return AppFlowyPopover( - controller: _popoverController, - mutex: widget.popoverMutex, - offset: const Offset(8, 0), - margin: EdgeInsets.zero, - asBarrier: true, - constraints: BoxConstraints.loose(const Size(460, 470)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - ), - child: child, - ), - ), - popupBuilder: (BuildContext popoverContext) { - return SelectOptionEditor( - option: widget.option, - onDeleted: () { - context - .read() - .add(SelectOptionTypeOptionEvent.deleteOption(widget.option)); - PopoverContainer.of(popoverContext).close(); - }, - onUpdated: (updatedOption) { - context - .read() - .add(SelectOptionTypeOptionEvent.updateOption(updatedOption)); - PopoverContainer.of(popoverContext).close(); - }, - key: ValueKey(widget.option.id), - ); - }, - ); - } -} - -class _AddOptionButton extends StatelessWidget { - const _AddOptionButton(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.grid_field_addSelectOption.tr(), - ), - onTap: () { - context - .read() - .add(const SelectOptionTypeOptionEvent.addingOption()); - }, - leftIcon: const FlowySvg(FlowySvgs.add_s), - ), - ), - ); - } -} - -class CreateOptionTextField extends StatefulWidget { - const CreateOptionTextField({super.key, this.popoverMutex}); - - final PopoverMutex? popoverMutex; - - @override - State createState() => _CreateOptionTextFieldState(); -} - -class _CreateOptionTextFieldState extends State { - 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(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final text = state.newOptionName ?? ''; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 14.0), - child: FlowyTextField( - autoClearWhenDone: true, - text: text, - focusNode: focusNode, - onCanceled: () { - context - .read() - .add(const SelectOptionTypeOptionEvent.endAddingOption()); - }, - onEditingComplete: () {}, - onSubmitted: (optionName) { - context - .read() - .add(SelectOptionTypeOptionEvent.createOption(optionName)); - }, - ), - ); - }, - ); - } - - void _onFocusChanged() { - if (focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - } - - void _onPopoverChanged() { - if (focusNode.hasFocus) { - focusNode.unfocus(); - } - } -} - -class _OptionList extends StatelessWidget { - const _OptionList({ - this.popoverMutex, - }); - - final PopoverMutex? popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return ReorderableListView.builder( - shrinkWrap: true, - onReorderStart: (_) => popoverMutex?.close(), - 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, - itemBuilder: (context, index) => _OptionCell( - key: ValueKey("select_type_option_list_${state.options[index].id}"), - index: index, - option: state.options[index], - popoverMutex: popoverMutex, - ), - itemCount: state.options.length, - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex--; - } - final fromOptionId = state.options[oldIndex].id; - final toOptionId = state.options[newIndex].id; - context.read().add( - SelectOptionTypeOptionEvent.reorderOption( - fromOptionId, - toOptionId, - ), - ); - }, - ); - }, - ); - } -} 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 deleted file mode 100644 index 9946a6ab75..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart +++ /dev/null @@ -1,244 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/edit_select_option_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.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/scrolling/styled_list.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../../../grid/presentation/layout/sizes.dart'; -import '../../../../grid/presentation/widgets/common/type_option_separator.dart'; - -class SelectOptionEditor extends StatelessWidget { - const SelectOptionEditor({ - super.key, - required this.option, - required this.onDeleted, - required this.onUpdated, - this.showOptions = true, - this.autoFocus = true, - }); - - final SelectOptionPB option; - final VoidCallback onDeleted; - final Function(SelectOptionPB) onUpdated; - final bool showOptions; - final bool autoFocus; - - static String get identifier => (SelectOptionEditor).toString(); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => EditSelectOptionBloc(option: option), - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => p.deleted != c.deleted, - listener: (context, state) { - if (state.deleted) { - onDeleted(); - } - }, - ), - BlocListener( - listenWhen: (p, c) => p.option != c.option, - listener: (context, state) { - onUpdated(state.option); - }, - ), - ], - child: BlocBuilder( - builder: (context, state) { - final List cells = [ - _OptionNameTextField( - name: state.option.name, - autoFocus: autoFocus, - ), - const VSpace(10), - const _DeleteTag(), - const TypeOptionSeparator(), - SelectOptionColorList( - selectedColor: state.option.color, - onSelectedColor: (color) => context - .read() - .add(EditSelectOptionEvent.updateColor(color)), - ), - ]; - return SizedBox( - width: 180, - child: ListView.builder( - shrinkWrap: true, - physics: StyledScrollPhysics(), - itemCount: cells.length, - itemBuilder: (context, index) { - if (cells[index] is TypeOptionSeparator) { - return cells[index]; - } else { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: cells[index], - ); - } - }, - padding: const EdgeInsets.symmetric(vertical: 6.0), - ), - ); - }, - ), - ), - ); - } -} - -class _DeleteTag extends StatelessWidget { - const _DeleteTag(); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.grid_selectOption_deleteTag.tr(), - ), - leftIcon: const FlowySvg(FlowySvgs.delete_s), - onTap: () { - context - .read() - .add(const EditSelectOptionEvent.delete()); - }, - ), - ); - } -} - -class _OptionNameTextField extends StatelessWidget { - const _OptionNameTextField({ - required this.name, - required this.autoFocus, - }); - - final String name; - final bool autoFocus; - - @override - Widget build(BuildContext context) { - return FlowyTextField( - autoFocus: autoFocus, - text: name, - submitOnLeave: true, - onSubmitted: (newName) { - if (name != newName) { - context - .read() - .add(EditSelectOptionEvent.updateName(newName)); - } - }, - ); - } -} - -class SelectOptionColorList extends StatelessWidget { - const SelectOptionColorList({ - super.key, - this.selectedColor, - required this.onSelectedColor, - }); - - final SelectOptionColorPB? selectedColor; - final void Function(SelectOptionColorPB color) onSelectedColor; - - @override - Widget build(BuildContext context) { - final cells = SelectOptionColorPB.values.map((color) { - return _SelectOptionColorCell( - color: color, - isSelected: selectedColor != null ? selectedColor == color : false, - onSelectedColor: onSelectedColor, - ); - }).toList(); - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: GridSize.typeOptionContentInsets, - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyText( - LocaleKeys.grid_selectOption_colorPanelTitle.tr(), - textAlign: TextAlign.left, - color: Theme.of(context).hintColor, - ), - ), - ), - ListView.separated( - shrinkWrap: true, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - itemCount: cells.length, - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, - ), - ], - ); - } -} - -class _SelectOptionColorCell extends StatelessWidget { - const _SelectOptionColorCell({ - required this.color, - required this.isSelected, - required this.onSelectedColor, - }); - - final SelectOptionColorPB color; - final bool isSelected; - final void Function(SelectOptionColorPB color) onSelectedColor; - - @override - Widget build(BuildContext context) { - Widget? checkmark; - if (isSelected) { - checkmark = const FlowySvg(FlowySvgs.check_s); - } - - final colorIcon = SizedBox.square( - dimension: 16, - child: DecoratedBox( - decoration: BoxDecoration( - color: color.toColor(context), - shape: BoxShape.circle, - ), - ), - ); - - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, - color.colorName(), - color: AFThemeExtension.of(context).textColor, - ), - leftIcon: colorIcon, - rightIcon: checkmark, - onTap: () => onSelectedColor(color), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/single_select.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/single_select.dart deleted file mode 100644 index a0ea917b21..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/single_select.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.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/material.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; - -import 'builder.dart'; -import 'select/select_option.dart'; - -class SingleSelectTypeOptionEditorFactory implements TypeOptionEditorFactory { - const SingleSelectTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - return SelectOptionTypeOptionWidget( - options: typeOption.options, - beginEdit: () => PopoverContainer.of(context).closeAll(), - popoverMutex: popoverMutex, - typeOptionAction: SingleSelectAction( - viewId: viewId, - fieldId: field.id, - onTypeOptionUpdated: onTypeOptionUpdated, - ), - ); - } - - SingleSelectTypeOptionPB _parseTypeOptionData(List data) { - return SingleSelectTypeOptionDataParser().fromBuffer(data); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/summary.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/summary.dart deleted file mode 100644 index 76a78aa22a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/summary.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; - -import 'builder.dart'; - -class SummaryTypeOptionEditorFactory implements TypeOptionEditorFactory { - const SummaryTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) => - null; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart deleted file mode 100644 index 01a8c519c2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; - -import 'builder.dart'; - -class TimeTypeOptionEditorFactory implements TypeOptionEditorFactory { - const TimeTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) => - null; -} 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 deleted file mode 100644 index e67929d2dc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart +++ /dev/null @@ -1,130 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:protobuf/protobuf.dart'; - -import 'builder.dart'; -import 'date/date_time_format.dart'; - -class TimestampTypeOptionEditorFactory implements TypeOptionEditorFactory { - const TimestampTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - return SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => VSpace(GridSize.typeOptionSeparatorHeight), - children: [ - _renderDateFormatButton(typeOption, popoverMutex, onTypeOptionUpdated), - _renderTimeFormatButton(typeOption, popoverMutex, onTypeOptionUpdated), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: IncludeTimeButton( - onChanged: (value) { - final newTypeOption = _updateTypeOption( - typeOption: typeOption, - includeTime: value, - ); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - }, - includeTime: typeOption.includeTime, - ), - ), - ], - ); - } - - Widget _renderDateFormatButton( - TimestampTypeOptionPB typeOption, - PopoverMutex popoverMutex, - TypeOptionDataCallback onTypeOptionUpdated, - ) { - return AppFlowyPopover( - mutex: popoverMutex, - asBarrier: true, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - constraints: BoxConstraints.loose(const Size(460, 440)), - popupBuilder: (popoverContext) { - return DateFormatList( - selectedFormat: typeOption.dateFormat, - onSelected: (format) { - final newTypeOption = - _updateTypeOption(typeOption: typeOption, dateFormat: format); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(popoverContext).close(); - }, - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: DateFormatButton(), - ), - ); - } - - Widget _renderTimeFormatButton( - TimestampTypeOptionPB typeOption, - PopoverMutex popoverMutex, - TypeOptionDataCallback onTypeOptionUpdated, - ) { - return AppFlowyPopover( - mutex: popoverMutex, - asBarrier: true, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - constraints: BoxConstraints.loose(const Size(460, 440)), - popupBuilder: (BuildContext popoverContext) { - return TimeFormatList( - selectedFormat: typeOption.timeFormat, - onSelected: (format) { - final newTypeOption = - _updateTypeOption(typeOption: typeOption, timeFormat: format); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(popoverContext).close(); - }, - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: TimeFormatButton(), - ), - ); - } - - TimestampTypeOptionPB _parseTypeOptionData(List data) { - return TimestampTypeOptionDataParser().fromBuffer(data); - } - - TimestampTypeOptionPB _updateTypeOption({ - required TimestampTypeOptionPB typeOption, - DateFormatPB? dateFormat, - TimeFormatPB? timeFormat, - bool? includeTime, - }) { - typeOption.freeze(); - return typeOption.rebuild((typeOption) { - if (dateFormat != null) { - typeOption.dateFormat = dateFormat; - } - - if (timeFormat != null) { - typeOption.timeFormat = timeFormat; - } - - if (includeTime != null) { - typeOption.includeTime = 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 deleted file mode 100644 index 70ce6e8049..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -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: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 './builder.dart'; - -class TranslateTypeOptionEditorFactory implements TypeOptionEditorFactory { - const TranslateTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = TranslateTypeOptionPB.fromBuffer(field.typeOptionData); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - LocaleKeys.grid_field_translateTo.tr(), - ), - const HSpace(6), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: BlocProvider( - create: (context) => TranslateTypeOptionBloc(option: typeOption), - child: BlocConsumer( - listenWhen: (previous, current) => - previous.option != current.option, - listener: (context, state) { - onTypeOptionUpdated(state.option.writeToBuffer()); - }, - builder: (context, state) { - return _wrapLanguageListPopover( - context, - state, - popoverMutex, - SelectLanguageButton( - language: state.language, - ), - ); - }, - ), - ), - ), - ], - ), - ); - } - - Widget _wrapLanguageListPopover( - BuildContext blocContext, - TranslateTypeOptionState state, - PopoverMutex popoverMutex, - Widget child, - ) { - return AppFlowyPopover( - mutex: popoverMutex, - asBarrier: true, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - constraints: BoxConstraints.loose(const Size(460, 440)), - popupBuilder: (popoverContext) { - return LanguageList( - onSelected: (language) { - blocContext - .read() - .add(TranslateTypeOptionEvent.selectLanguage(language)); - PopoverContainer.of(popoverContext).close(); - }, - selectedLanguage: state.option.language, - ); - }, - child: child, - ); - } -} - -class SelectLanguageButton extends StatelessWidget { - const SelectLanguageButton({required this.language, super.key}); - final String language; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 30, - child: FlowyButton( - text: FlowyText( - language, - lineHeight: 1.0, - ), - ), - ); - } -} - -class LanguageList extends StatelessWidget { - const LanguageList({ - super.key, - required this.onSelected, - required this.selectedLanguage, - }); - - final Function(TranslateLanguagePB) onSelected; - final TranslateLanguagePB selectedLanguage; - - @override - Widget build(BuildContext context) { - final cells = TranslateLanguagePB.values.map((languageType) { - return LanguageCell( - languageType: languageType, - onSelected: onSelected, - isSelected: languageType == selectedLanguage, - ); - }).toList(); - - return SizedBox( - width: 180, - child: ListView.separated( - shrinkWrap: true, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - itemCount: cells.length, - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, - ), - ); - } -} - -class LanguageCell extends StatelessWidget { - const LanguageCell({ - required this.languageType, - required this.onSelected, - required this.isSelected, - super.key, - }); - final Function(TranslateLanguagePB) onSelected; - final TranslateLanguagePB languageType; - final bool isSelected; - - @override - Widget build(BuildContext context) { - Widget? checkmark; - if (isSelected) { - checkmark = const FlowySvg(FlowySvgs.check_s); - } - - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - languageTypeToLanguage(languageType), - lineHeight: 1.0, - ), - rightIcon: checkmark, - onTap: () => onSelected(languageType), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/url.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/url.dart deleted file mode 100644 index 6dc95d8824..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/url.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; -import 'builder.dart'; - -class URLTypeOptionEditorFactory implements TypeOptionEditorFactory { - const URLTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) => - null; -} 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 deleted file mode 100644 index f1486094bf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart +++ /dev/null @@ -1,225 +0,0 @@ -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/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_backend/protobuf/flowy-database2/protobuf.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/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:protobuf/protobuf.dart' hide FieldInfo; - -class DatabaseGroupList extends StatelessWidget { - const DatabaseGroupList({ - super.key, - required this.viewId, - required this.databaseController, - required this.onDismissed, - }); - - final String viewId; - final DatabaseController databaseController; - final VoidCallback onDismissed; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DatabaseGroupBloc( - viewId: viewId, - databaseController: databaseController, - )..add(const DatabaseGroupEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final field = state.fieldInfos.firstWhereOrNull( - (field) => field.fieldType.canBeGroup && field.isGroupField, - ); - final showHideUngroupedToggle = - field?.fieldType != FieldType.Checkbox; - - DateGroupConfigurationPB? config; - if (field != null) { - final gs = state.groupSettings - .firstWhereOrNull((gs) => gs.fieldId == field.id); - config = gs != null - ? DateGroupConfigurationPB.fromBuffer(gs.content) - : null; - } - - 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, - ), - ), - ), - ), - const TypeOptionSeparator(spacing: 0), - ], - SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: FlowyText( - LocaleKeys.board_groupBy.tr(), - textAlign: TextAlign.left, - color: Theme.of(context).hintColor, - ), - ), - ), - ...state.fieldInfos - .where((fieldInfo) => fieldInfo.fieldType.canBeGroup) - .map( - (fieldInfo) => _GridGroupCell( - fieldInfo: fieldInfo, - name: fieldInfo.name, - checked: fieldInfo.isGroupField, - onSelected: onDismissed, - key: ValueKey(fieldInfo.id), - ), - ), - if (field?.fieldType.groupConditions.isNotEmpty ?? false) ...[ - const TypeOptionSeparator(spacing: 0), - SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: FlowyText( - LocaleKeys.board_groupCondition.tr(), - textAlign: TextAlign.left, - color: Theme.of(context).hintColor, - ), - ), - ), - ...field!.fieldType.groupConditions.map( - (condition) => _GridGroupCell( - fieldInfo: field, - name: condition.name, - condition: condition.value, - onSelected: onDismissed, - checked: config?.condition == condition, - ), - ), - ], - ]; - - return ListView.separated( - shrinkWrap: true, - itemCount: children.length, - itemBuilder: (BuildContext context, int index) => children[index], - separatorBuilder: (BuildContext context, int index) => - VSpace(GridSize.typeOptionSeparatorHeight), - padding: const EdgeInsets.symmetric(vertical: 6.0), - ); - }, - ), - ); - } - - Future _updateLayoutSettings( - BoardLayoutSettingPB layoutSettings, - bool hideUngrouped, - ) { - layoutSettings.freeze(); - final newLayoutSetting = layoutSettings.rebuild((message) { - message.hideUngroupedColumn = hideUngrouped; - }); - return databaseController.updateLayoutSetting( - boardLayoutSetting: newLayoutSetting, - ); - } -} - -class _GridGroupCell extends StatelessWidget { - const _GridGroupCell({ - super.key, - required this.fieldInfo, - required this.onSelected, - required this.checked, - required this.name, - this.condition = 0, - }); - - final FieldInfo fieldInfo; - final VoidCallback onSelected; - final bool checked; - final int condition; - final String name; - - @override - Widget build(BuildContext context) { - Widget? rightIcon; - if (checked) { - rightIcon = const Padding( - padding: EdgeInsets.all(2.0), - child: FlowySvg(FlowySvgs.check_s), - ); - } - - return SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - name, - color: AFThemeExtension.of(context).textColor, - lineHeight: 1.0, - ), - leftIcon: FieldIcon(fieldInfo: fieldInfo), - rightIcon: rightIcon, - onTap: () { - List settingContent = []; - switch (fieldInfo.fieldType) { - case FieldType.DateTime: - final config = DateGroupConfigurationPB() - ..condition = DateConditionPB.values[condition]; - settingContent = config.writeToBuffer(); - break; - default: - } - context.read().add( - DatabaseGroupEvent.setGroupByField( - fieldInfo.id, - fieldInfo.fieldType, - settingContent, - ), - ); - onSelected(); - }, - ), - ), - ); - } -} 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 deleted file mode 100644 index 6e13cc5ecb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart +++ /dev/null @@ -1,196 +0,0 @@ -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:styled_widget/styled_widget.dart'; - -import '../../cell/editable_cell_builder.dart'; - -class GridCellAccessoryBuildContext { - GridCellAccessoryBuildContext({ - required this.anchorContext, - required this.isCellEditing, - }); - - final BuildContext anchorContext; - final bool isCellEditing; -} - -class GridCellAccessoryBuilder> { - GridCellAccessoryBuilder({required Widget Function(Key key) builder}) - : _builder = builder; - - final GlobalKey _key = GlobalKey(); - - final Widget Function(Key key) _builder; - - Widget build() => _builder(_key); - - void onTap() { - (_key.currentState as GridCellAccessoryState).onTap(); - } - - bool enable() { - if (_key.currentState == null) { - return true; - } - return (_key.currentState as GridCellAccessoryState).enable(); - } -} - -abstract mixin class GridCellAccessoryState { - void onTap(); - - // The accessory will be hidden if enable() return false; - bool enable() => true; -} - -class PrimaryCellAccessory extends StatefulWidget { - const PrimaryCellAccessory({ - super.key, - required this.onTap, - required this.isCellEditing, - }); - - final VoidCallback onTap; - final bool isCellEditing; - - @override - State createState() => _PrimaryCellAccessoryState(); -} - -class _PrimaryCellAccessoryState extends State - with GridCellAccessoryState { - @override - Widget build(BuildContext context) { - return FlowyHover( - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - backgroundColor: Theme.of(context).cardColor, - ), - builder: (_, onHover) { - return FlowyTooltip( - message: LocaleKeys.tooltip_openAsPage.tr(), - child: Container( - width: 26, - height: 26, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: Corners.s6Border, - ), - child: Center( - child: FlowySvg( - FlowySvgs.full_view_s, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ); - }, - ); - } - - @override - void onTap() => widget.onTap(); - - @override - bool enable() => !widget.isCellEditing; -} - -class AccessoryHover extends StatefulWidget { - const AccessoryHover({ - super.key, - required this.child, - required this.fieldType, - }); - - final CellAccessory child; - final FieldType fieldType; - - @override - State createState() => _AccessoryHoverState(); -} - -class _AccessoryHoverState extends State { - bool _isHover = false; - - @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( - color: _isHover && widget.fieldType != FieldType.Checklist - ? AFThemeExtension.of(context).lightGreyHover - : Colors.transparent, - borderRadius: Corners.s6Border, - ), - child: widget.child, - ), - ]; - - final accessoryBuilder = widget.child.accessoryBuilder; - if (accessoryBuilder != null && _isHover) { - final accessories = accessoryBuilder( - GridCellAccessoryBuildContext( - anchorContext: context, - isCellEditing: false, - ), - ); - children.add( - Padding( - padding: const EdgeInsets.only(right: 6), - child: CellAccessoryContainer(accessories: accessories), - ).positioned(right: 0), - ); - } - - return MouseRegion( - cursor: SystemMouseCursors.click, - opaque: false, - onEnter: (p) => setState(() => _isHover = true), - onExit: (p) => setState(() => _isHover = false), - child: Stack( - alignment: AlignmentDirectional.center, - children: children, - ), - ); - } -} - -class CellAccessoryContainer extends StatelessWidget { - const CellAccessoryContainer({required this.accessories, super.key}); - - final List accessories; - - @override - Widget build(BuildContext context) { - final children = - accessories.where((accessory) => accessory.enable()).map((accessory) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => accessory.onTap(), - child: accessory.build(), - ); - }).toList(); - - return SeparatedRow( - separatorBuilder: () => const HSpace(6), - children: children, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart deleted file mode 100644 index 3fc2131f24..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -typedef CellKeyboardAction = dynamic Function(); - -enum CellKeyboardKey { - onEnter, - onCopy, - onInsert, -} - -abstract class CellShortcuts extends Widget { - const CellShortcuts({super.key}); - - Map get shortcutHandlers; -} - -class GridCellShortcuts extends StatelessWidget { - const GridCellShortcuts({required this.child, super.key}); - - final CellShortcuts child; - - @override - Widget build(BuildContext context) { - return Shortcuts( - shortcuts: shortcuts, - child: Actions( - actions: actions, - child: child, - ), - ); - } - - Map get shortcuts => { - if (shouldAddKeyboardKey(CellKeyboardKey.onEnter)) - LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(), - if (shouldAddKeyboardKey(CellKeyboardKey.onCopy)) - LogicalKeySet( - Platform.isMacOS - ? LogicalKeyboardKey.meta - : LogicalKeyboardKey.control, - LogicalKeyboardKey.keyC, - ): const GridCellCopyIntent(), - }; - - Map> get actions => { - if (shouldAddKeyboardKey(CellKeyboardKey.onEnter)) - GridCellEnterIdent: GridCellEnterAction(child: child), - if (shouldAddKeyboardKey(CellKeyboardKey.onCopy)) - GridCellCopyIntent: GridCellCopyAction(child: child), - }; - - bool shouldAddKeyboardKey(CellKeyboardKey key) => - child.shortcutHandlers.containsKey(key); -} - -class GridCellEnterIdent extends Intent { - const GridCellEnterIdent(); -} - -class GridCellEnterAction extends Action { - GridCellEnterAction({required this.child}); - - final CellShortcuts child; - - @override - void invoke(covariant GridCellEnterIdent intent) { - final callback = child.shortcutHandlers[CellKeyboardKey.onEnter]; - if (callback != null) { - callback(); - } - } -} - -class GridCellCopyIntent extends Intent { - const GridCellCopyIntent(); -} - -class GridCellCopyAction extends Action { - GridCellCopyAction({required this.child}); - - final CellShortcuts child; - - @override - void invoke(covariant GridCellCopyIntent intent) { - final callback = child.shortcutHandlers[CellKeyboardKey.onCopy]; - if (callback == null) { - return; - } - - final s = callback(); - if (s is String) { - Clipboard.setData(ClipboardData(text: s)); - } - } -} 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 deleted file mode 100644 index 333ff0fe96..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart +++ /dev/null @@ -1,157 +0,0 @@ -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'; - -class CellContainer extends StatelessWidget { - const CellContainer({ - super.key, - required this.child, - required this.width, - required this.isPrimary, - this.accessoryBuilder, - }); - - final EditableCellWidget child; - final AccessoryBuilder? accessoryBuilder; - final double width; - final bool isPrimary; - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: child.cellContainerNotifier, - child: Selector( - selector: (context, notifier) => notifier.isFocus, - builder: (providerContext, isFocus, _) { - Widget container = Center(child: GridCellShortcuts(child: child)); - - if (accessoryBuilder != null) { - final accessories = accessoryBuilder!.call( - GridCellAccessoryBuildContext( - anchorContext: context, - isCellEditing: isFocus, - ), - ); - - if (accessories.isNotEmpty) { - container = _GridCellEnterRegion( - accessories: accessories, - isPrimary: isPrimary, - child: container, - ); - } - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (!isFocus) { - child.requestFocus.notify(); - } - }, - child: Container( - constraints: BoxConstraints(maxWidth: width, minHeight: 32), - decoration: _makeBoxDecoration(context, isFocus), - child: container, - ), - ); - }, - ), - ); - } - - BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) { - if (isFocus) { - final borderSide = BorderSide( - color: Theme.of(context).colorScheme.primary, - ); - - return BoxDecoration(border: Border.fromBorderSide(borderSide)); - } - - final borderSide = - BorderSide(color: AFThemeExtension.of(context).borderColor); - return BoxDecoration( - border: Border(right: borderSide, bottom: borderSide), - ); - } -} - -class _GridCellEnterRegion extends StatelessWidget { - const _GridCellEnterRegion({ - required this.child, - required this.accessories, - required this.isPrimary, - }); - - final Widget child; - final List accessories; - final bool isPrimary; - - @override - Widget build(BuildContext context) { - return Selector2( - selector: (context, regionNotifier, cellNotifier) => - !cellNotifier.isFocus && - (cellNotifier.isHover || regionNotifier.onEnter && isPrimary), - builder: (context, showAccessory, _) { - final List children = [child]; - - if (showAccessory) { - children.add( - CellAccessoryContainer(accessories: accessories).positioned( - right: GridSize.cellContentInsets.right, - ), - ); - } - - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (p) => - CellContainerNotifier.of(context, listen: false).isHover = true, - onExit: (p) => - CellContainerNotifier.of(context, listen: false).isHover = false, - child: Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: children, - ), - ); - }, - ); - } -} - -class CellContainerNotifier extends ChangeNotifier { - bool _isFocus = false; - bool _onEnter = false; - - set isFocus(bool value) { - if (_isFocus != value) { - _isFocus = value; - notifyListeners(); - } - } - - set isHover(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get isFocus => _isFocus; - - bool get isHover => _onEnter; - - static CellContainerNotifier of(BuildContext context, {bool listen = true}) { - return Provider.of(context, listen: listen); - } -} 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 deleted file mode 100644 index 8dba996d05..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/mobile_cell_container.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../cell/editable_cell_builder.dart'; -import 'cell_container.dart'; - -class MobileCellContainer extends StatelessWidget { - const MobileCellContainer({ - super.key, - required this.child, - required this.isPrimary, - this.onPrimaryFieldCellTap, - }); - - final EditableCellWidget child; - final bool isPrimary; - final VoidCallback? onPrimaryFieldCellTap; - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: child.cellContainerNotifier, - child: Selector( - selector: (context, notifier) => notifier.isFocus, - builder: (providerContext, isFocus, _) { - Widget container = Center(child: child); - - if (isPrimary) { - container = IgnorePointer(child: container); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (isPrimary) { - onPrimaryFieldCellTap?.call(); - return; - } - if (!isFocus) { - child.requestFocus.notify(); - } - }, - child: Container( - constraints: const BoxConstraints(maxWidth: 200, minHeight: 46), - decoration: _makeBoxDecoration(context, isPrimary, isFocus), - child: container, - ), - ); - }, - ), - ); - } - - BoxDecoration _makeBoxDecoration( - BuildContext context, - bool isPrimary, - bool isFocus, - ) { - if (isFocus) { - return BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - ); - } - - final borderSide = BorderSide(color: Theme.of(context).dividerColor); - return BoxDecoration( - border: Border( - left: isPrimary ? borderSide : BorderSide.none, - right: borderSide, - bottom: borderSide, - ), - ); - } -} 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 deleted file mode 100644 index 1260641fdf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart +++ /dev/null @@ -1,44 +0,0 @@ -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'; - -import 'row_detail.dart'; - -class RelatedRowDetailPage extends StatelessWidget { - const RelatedRowDetailPage({ - super.key, - required this.databaseId, - required this.rowId, - }); - - final String databaseId; - final String rowId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => RelatedRowDetailPageBloc( - databaseId: databaseId, - initialRowId: rowId, - ), - child: BlocBuilder( - builder: (_, 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, - ), - ); - }, - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index c0cf547a06..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.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/grid/presentation/layout/sizes.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class RowActionList extends StatelessWidget { - const RowActionList({super.key, required this.rowController}); - - final RowController rowController; - - @override - Widget build(BuildContext context) { - return IntrinsicWidth( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - RowDetailPageDuplicateButton( - viewId: rowController.viewId, - rowId: rowController.rowId, - ), - const VSpace(4.0), - RowDetailPageDeleteButton( - viewId: rowController.viewId, - rowId: rowController.rowId, - ), - ], - ), - ); - } -} - -class RowDetailPageDeleteButton extends StatelessWidget { - const RowDetailPageDeleteButton({ - super.key, - required this.viewId, - required this.rowId, - }); - - final String viewId; - final String rowId; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.regular( - LocaleKeys.grid_row_delete.tr(), - lineHeight: 1.0, - ), - leftIcon: const FlowySvg(FlowySvgs.trash_m), - onTap: () { - RowBackendService.deleteRows(viewId, [rowId]); - FlowyOverlay.pop(context); - }, - ), - ); - } -} - -class RowDetailPageDuplicateButton extends StatelessWidget { - const RowDetailPageDuplicateButton({ - super.key, - required this.viewId, - required this.rowId, - }); - - final String viewId; - final String rowId; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.regular( - LocaleKeys.grid_row_duplicate.tr(), - lineHeight: 1.0, - ), - leftIcon: const FlowySvg(FlowySvgs.copy_s), - onTap: () { - RowBackendService.duplicateRow(viewId, rowId); - FlowyOverlay.pop(context); - }, - ), - ); - } -} 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 deleted file mode 100644 index debbb467e7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ /dev/null @@ -1,685 +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/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: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: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; - -class RowBanner extends StatefulWidget { - const RowBanner({ - super.key, - required this.databaseController, - 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(); -} - -class _RowBannerState extends State { - final _isHovering = ValueNotifier(false); - late final isLocalMode = - (widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) == - AuthTypePB.Local; - - @override - void dispose() { - _isHovering.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => RowBannerBloc( - viewId: widget.rowController.viewId, - 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( - 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)); - } - }, - ), - ), - ], - ); - }, - ), - const VSpace(8), - _BannerTitle( - cellBuilder: widget.cellBuilder, - 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, - }); - - 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; - - @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), - ], - ), - ), - ); - } - - 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, - ], - 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), - ); - } -} - -class _BannerTitle extends StatelessWidget { - const _BannerTitle({ - required this.cellBuilder, - required this.rowController, - }); - - final EditableCellBuilder cellBuilder; - final RowController rowController; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final children = [ - if (state.primaryField != null) - Expanded( - child: cellBuilder.buildCustom( - CellContext( - fieldId: state.primaryField!.id, - rowId: rowController.rowId, - ), - skinMap: EditableCellSkinMap(textSkin: _TitleSkin()), - ), - ), - ]; - - return Padding( - padding: const EdgeInsets.only(left: 60), - child: Row(children: children), - ); - }, - ); - } -} - -class _TitleSkin extends IEditableTextCellSkin { - @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, - ), - onEditingComplete: () { - bloc.add(TextCellEvent.updateText(textEditingController.text)); - }, - ), - ); - } -} - -class RowActionButton extends StatelessWidget { - const RowActionButton({super.key, required this.rowController}); - - final RowController rowController; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (context) => RowActionList(rowController: rowController), - child: FlowyTooltip( - message: LocaleKeys.grid_rowPage_moreRowActions.tr(), - child: FlowyIconButton( - width: 20, - height: 20, - icon: const FlowySvg(FlowySvgs.details_horizontal_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } -} 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 deleted file mode 100644 index 8bd181b427..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.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/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/row/row_controller.dart'; -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: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'; - -class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { - const RowDetailPage({ - super.key, - 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(); - - 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(); - } - - @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, - ), - ), - BlocProvider.value(value: getIt()), - ], - child: BlocBuilder( - builder: (context, state) => Stack( - fit: StackFit.expand, - 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, - ), - ), - ), - Positioned( - top: calculateActionsOffset( - state.rowMeta.cover.data.isNotEmpty, - ), - 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) { - return [ - if (widget.allowOpenAsFullPage) ...[ - FlowyTooltip( - message: LocaleKeys.grid_rowPage_openAsFullPage.tr(), - child: FlowyIconButton( - width: 20, - height: 20, - icon: const FlowySvg(FlowySvgs.full_view_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - onPressed: () async { - Navigator.of(context).pop(); - final databaseId = await DatabaseViewBackendService( - viewId: widget.databaseController.viewId, - ) - .getDatabaseId() - .then((value) => value.fold((s) => s, (f) => null)); - final documentId = widget.rowController.rowMeta.documentId; - if (databaseId != null) { - getIt().add( - TabsEvent.openPlugin( - plugin: DatabaseDocumentPlugin( - data: DatabaseDocumentContext( - view: widget.databaseController.view, - databaseId: databaseId, - rowId: widget.rowController.rowId, - documentId: documentId, - ), - pluginType: PluginType.databaseDocument, - ), - setLatest: false, - ), - ); - } - }, - ), - ), - const HSpace(4), - ], - RowActionButton(rowController: widget.rowController), - ]; - } -} 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 deleted file mode 100644 index 436dbd085d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart +++ /dev/null @@ -1,163 +0,0 @@ -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:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; - -class RowDocument extends StatelessWidget { - const RowDocument({ - super.key, - required this.viewId, - required this.rowId, - }); - - final String viewId; - final String rowId; - - @override - Widget build(BuildContext context) { - 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, - ), - builder: (context, state) { - return state.loadingState.when( - loading: () => const Center( - child: CircularProgressIndicator.adaptive(), - ), - error: (error) => Center( - child: AppFlowyErrorPage( - error: error, - ), - ), - finish: () => _RowEditor( - view: state.viewPB!, - onIsEmptyChanged: (isEmpty) => context - .read() - .add(RowDocumentEvent.updateIsEmpty(isEmpty)), - ), - ); - }, - ), - ); - } -} - -class _RowEditor extends StatelessWidget { - const _RowEditor({ - required this.view, - this.onIsEmptyChanged, - }); - - final ViewPB view; - final void Function(bool)? onIsEmptyChanged; - - @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( - listenWhen: (previous, current) => - previous.isDocumentEmpty != current.isDocumentEmpty, - listener: (_, 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'); - } - }, - 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), - ); - } - - 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, - 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(), - ), - ), - ), - ), - ), - ), - ), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 240d33f0f2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ /dev/null @@ -1,407 +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/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/domain/field_service.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/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: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/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'; - -/// Display the row properties in a list. Only used in [RowDetailPage]. -class RowPropertyList extends StatelessWidget { - const RowPropertyList({ - super.key, - required this.viewId, - required this.fieldController, - required this.cellBuilder, - }); - - final String viewId; - final FieldController fieldController; - final EditableCellBuilder cellBuilder; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.showHiddenFields != current.showHiddenFields || - !listEquals(previous.visibleCells, current.visibleCells), - builder: (context, state) { - final children = state.visibleCells - .mapIndexed( - (index, cell) => _PropertyCell( - key: ValueKey('row_detail_${cell.fieldId}'), - cellContext: cell, - cellBuilder: cellBuilder, - fieldController: fieldController, - index: index, - ), - ) - .toList(); - - return ReorderableListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - onReorder: (from, to) => context - .read() - .add(RowDetailEvent.reorderField(from, to)), - buildDefaultDragHandles: false, - proxyDecorator: (child, index, animation) => 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( - width: 16, - height: 30, - child: FlowySvg(FlowySvgs.drag_element_s), - ), - ), - ], - ), - ), - footer: Padding( - padding: const EdgeInsets.only(left: 20), - child: Column( - children: [ - if (context.watch().state.numHiddenFields != 0) - const Padding( - padding: EdgeInsets.only(bottom: 4.0), - child: ToggleHiddenFieldsVisibilityButton(), - ), - CreateRowFieldButton( - viewId: viewId, - fieldController: fieldController, - ), - ], - ), - ), - children: children, - ); - }, - ); - } -} - -class _PropertyCell extends StatefulWidget { - const _PropertyCell({ - super.key, - required this.cellContext, - required this.cellBuilder, - required this.fieldController, - required this.index, - }); - - final CellContext cellContext; - final EditableCellBuilder cellBuilder; - final FieldController fieldController; - final int index; - - @override - State createState() => _PropertyCellState(); -} - -class _PropertyCellState extends State<_PropertyCell> { - final PopoverController _popoverController = PopoverController(); - - final ValueNotifier _isFieldHover = ValueNotifier(false); - - @override - Widget build(BuildContext context) { - final cell = widget.cellBuilder.buildStyled( - widget.cellContext, - EditableCellStyle.desktopRowDetail, - ); - final gesture = GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => cell.requestFocus.notify(), - child: AccessoryHover( - fieldType: widget.fieldController - .getField(widget.cellContext.fieldId)! - .fieldType, - child: cell, - ), - ); - - return Container( - margin: const EdgeInsets.only(bottom: 8), - constraints: const BoxConstraints(minHeight: 30), - child: MouseRegion( - onEnter: (event) { - _isFieldHover.value = true; - cell.cellContainerNotifier.isHover = true; - }, - onExit: (event) { - _isFieldHover.value = false; - cell.cellContainerNotifier.isHover = false; - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ValueListenableBuilder( - valueListenable: _isFieldHover, - builder: (context, value, _) { - return ReorderableDragStartListener( - index: widget.index, - enabled: value, - child: _buildDragHandle(context), - ); - }, - ), - const HSpace(4), - _buildFieldButton(context), - const HSpace(8), - Expanded(child: gesture), - ], - ), - ), - ); - } - - 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 { - const ToggleHiddenFieldsVisibilityButton({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.showHiddenFields != current.showHiddenFields || - previous.numHiddenFields != current.numHiddenFields, - builder: (context, state) { - final text = state.showHiddenFields - ? LocaleKeys.grid_rowPage_hideHiddenFields.plural( - state.numHiddenFields, - namedArgs: {'count': '${state.numHiddenFields}'}, - ) - : LocaleKeys.grid_rowPage_showHiddenFields.plural( - state.numHiddenFields, - namedArgs: {'count': '${state.numHiddenFields}'}, - ); - final quarterTurns = state.showHiddenFields ? 1 : 3; - return UniversalPlatform.isDesktopOrWeb - ? _desktop(context, text, quarterTurns) - : _mobile(context, text, quarterTurns); - }, - ); - } - - Widget _desktop(BuildContext context, String text, int quarterTurns) { - return SizedBox( - height: 30, - child: FlowyButton( - text: FlowyText( - text, - lineHeight: 1.0, - color: Theme.of(context).hintColor, - ), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - leftIcon: RotatedBox( - quarterTurns: quarterTurns, - child: FlowySvg( - FlowySvgs.arrow_left_s, - color: Theme.of(context).hintColor, - ), - ), - margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), - onTap: () => context.read().add( - const RowDetailEvent.toggleHiddenFieldVisibility(), - ), - ), - ); - } - - Widget _mobile(BuildContext context, String text, int quarterTurns) { - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: double.infinity), - 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(vertical: 14, horizontal: 6), - ), - ), - label: FlowyText( - text, - fontSize: 15, - color: Theme.of(context).hintColor, - ), - onPressed: () => context - .read() - .add(const RowDetailEvent.toggleHiddenFieldVisibility()), - icon: RotatedBox( - quarterTurns: quarterTurns, - child: FlowySvg( - FlowySvgs.arrow_left_s, - color: Theme.of(context).hintColor, - ), - ), - ), - ); - } -} - -class CreateRowFieldButton extends StatelessWidget { - const CreateRowFieldButton({ - super.key, - required this.viewId, - required this.fieldController, - }); - - final String viewId; - final FieldController fieldController; - - @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, - ), - ), - ); - } -} 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 deleted file mode 100644 index b4ee4134c9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart +++ /dev/null @@ -1,138 +0,0 @@ -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 { - const DatabaseLayoutSelector({ - super.key, - required this.viewId, - required this.databaseController, - }); - - final String viewId; - final DatabaseController databaseController; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DatabaseLayoutBloc( - viewId: viewId, - databaseLayout: databaseController.databaseLayout, - )..add(const DatabaseLayoutEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final cells = DatabaseLayoutPB.values - .map( - (layout) => DatabaseViewLayoutCell( - databaseLayout: layout, - isSelected: state.databaseLayout == layout, - onTap: (selectedLayout) => context - .read() - .add(DatabaseLayoutEvent.updateLayout(selectedLayout)), - ), - ) - .toList(); - return Padding( - 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, - ); - }, - ), - ), - ), - ), - ], - ), - ); - }, - ), - ); - } -} - -class DatabaseViewLayoutCell extends StatelessWidget { - const DatabaseViewLayoutCell({ - super.key, - required this.isSelected, - required this.databaseLayout, - required this.onTap, - }); - - final bool isSelected; - final DatabaseLayoutPB databaseLayout; - final void Function(DatabaseLayoutPB) onTap; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: SizedBox( - height: 30, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, - databaseLayout.layoutName, - color: AFThemeExtension.of(context).textColor, - ), - leftIcon: FlowySvg( - databaseLayout.icon, - color: Theme.of(context).iconTheme.color, - ), - rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null, - onTap: () => onTap(databaseLayout), - ), - ), - ); - } -} 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 deleted file mode 100644 index c7bc286371..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.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/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/setting/setting_property_list.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'; - -enum DatabaseSettingAction { - showProperties, - showLayout, - showGroup, - showCalendarLayout, -} - -extension DatabaseSettingActionExtension on DatabaseSettingAction { - FlowySvgData iconData() { - switch (this) { - case DatabaseSettingAction.showProperties: - return FlowySvgs.multiselect_s; - case DatabaseSettingAction.showLayout: - return FlowySvgs.database_layout_s; - case DatabaseSettingAction.showGroup: - return FlowySvgs.group_s; - case DatabaseSettingAction.showCalendarLayout: - return FlowySvgs.calendar_layout_s; - } - } - - String title() { - switch (this) { - case DatabaseSettingAction.showProperties: - return LocaleKeys.grid_settings_properties.tr(); - case DatabaseSettingAction.showLayout: - return LocaleKeys.grid_settings_databaseLayout.tr(); - case DatabaseSettingAction.showGroup: - return LocaleKeys.grid_settings_group.tr(); - case DatabaseSettingAction.showCalendarLayout: - return LocaleKeys.calendar_settings_name.tr(); - } - } - - Widget build( - BuildContext context, - DatabaseController databaseController, - PopoverMutex popoverMutex, - ) { - final popover = switch (this) { - DatabaseSettingAction.showLayout => DatabaseLayoutSelector( - viewId: databaseController.viewId, - databaseController: databaseController, - ), - DatabaseSettingAction.showGroup => DatabaseGroupList( - viewId: databaseController.viewId, - databaseController: databaseController, - onDismissed: () {}, - ), - DatabaseSettingAction.showProperties => DatabasePropertyList( - viewId: databaseController.viewId, - fieldController: databaseController.fieldController, - ), - DatabaseSettingAction.showCalendarLayout => CalendarLayoutSetting( - databaseController: databaseController, - ), - }; - - return AppFlowyPopover( - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - direction: PopoverDirection.leftWithTopAligned, - mutex: popoverMutex, - margin: EdgeInsets.zero, - offset: const Offset(-14, 0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - 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 deleted file mode 100644 index 79d5e2410e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart +++ /dev/null @@ -1,80 +0,0 @@ -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_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({ - super.key, - required this.databaseController, - }); - - final DatabaseController databaseController; - - @override - State createState() => _DatabaseSettingsListState(); -} - -class _DatabaseSettingsListState extends State { - final PopoverMutex popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final cells = - actionsForDatabaseLayout(widget.databaseController.databaseLayout) - .map( - (action) => action.build( - context, - widget.databaseController, - popoverMutex, - ), - ) - .toList(); - - return ListView.separated( - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: cells.length, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) => cells[index], - ); - } -} - -/// Returns the list of actions that should be shown for the given database layout. -List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { - switch (layout) { - case DatabaseLayoutPB.Board: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - if (!UniversalPlatform.isMobile) DatabaseSettingAction.showGroup, - ]; - case DatabaseLayoutPB.Calendar: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - DatabaseSettingAction.showCalendarLayout, - ]; - case DatabaseLayoutPB.Grid: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - ]; - default: - return []; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/field_visibility_extension.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/field_visibility_extension.dart deleted file mode 100644 index 9fe9061205..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/field_visibility_extension.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; - -extension ToggleVisibility on FieldVisibility { - FieldVisibility toggle() => switch (this) { - FieldVisibility.AlwaysShown => FieldVisibility.AlwaysHidden, - FieldVisibility.AlwaysHidden => FieldVisibility.AlwaysShown, - _ => FieldVisibility.AlwaysHidden, - }; - - bool isVisibleState() => switch (this) { - FieldVisibility.AlwaysShown => true, - FieldVisibility.HideWhenEmpty => true, - FieldVisibility.AlwaysHidden => false, - _ => false, - }; -} 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 deleted file mode 100644 index 6394a2ac1a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart +++ /dev/null @@ -1,188 +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/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/sort/sort_editor_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.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'; - -enum MobileDatabaseControlFeatures { sort, filter } - -class MobileDatabaseControls extends StatelessWidget { - const MobileDatabaseControls({ - super.key, - required this.controller, - required this.features, - }); - - final DatabaseController controller; - final List features; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => FilterEditorBloc( - viewId: controller.viewId, - fieldController: controller.fieldController, - ), - ), - 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(); - } - - return SeparatedRow( - separatorBuilder: () => const HSpace(8.0), - children: [ - if (features.contains(MobileDatabaseControlFeatures.sort)) - _DatabaseControlButton( - icon: FlowySvgs.sort_ascending_s, - count: context.watch().state.sorts.length, - onTap: () => _showEditSortPanelFromToolbar( - context, - controller, - ), - ), - if (features.contains(MobileDatabaseControlFeatures.filter)) - _DatabaseControlButton( - icon: FlowySvgs.filter_s, - count: context.watch().state.filters.length, - onTap: () => _showEditFilterPanelFromToolbar( - context, - controller, - ), - ), - _DatabaseControlButton( - icon: FlowySvgs.m_field_hide_s, - onTap: () => _showDatabaseFieldListFromToolbar( - context, - controller, - ), - ), - ], - ); - }, - ), - ); - } -} - -class _DatabaseControlButton extends StatelessWidget { - const _DatabaseControlButton({ - required this.onTap, - required this.icon, - this.count = 0, - }); - - final VoidCallback onTap; - final FlowySvgData icon; - final int count; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(10), - child: Padding( - padding: const EdgeInsets.all(5.0), - child: count == 0 - ? FlowySvg( - icon, - size: const Size.square(20), - ) - : Row( - children: [ - FlowySvg( - icon, - size: const Size.square(20), - color: Theme.of(context).colorScheme.primary, - ), - const HSpace(2.0), - FlowyText.medium( - count.toString(), - color: Theme.of(context).colorScheme.primary, - ), - ], - ), - ), - ); - } -} - -void _showDatabaseFieldListFromToolbar( - BuildContext context, - DatabaseController databaseController, -) { - showTransitionMobileBottomSheet( - context, - showHeader: true, - showBackButton: true, - title: LocaleKeys.grid_settings_properties.tr(), - builder: (_) { - return BlocProvider.value( - value: context.read(), - child: MobileDatabaseFieldList( - databaseController: databaseController, - canCreate: false, - ), - ); - }, - ); -} - -void _showEditSortPanelFromToolbar( - 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 MobileSortEditor(), - ); - }, - ); -} - -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 deleted file mode 100644 index 36a6436b2a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart +++ /dev/null @@ -1,46 +0,0 @@ -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/widgets/setting/database_settings_list.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'; - -class SettingButton extends StatefulWidget { - const SettingButton({super.key, required this.databaseController}); - - final DatabaseController databaseController; - - @override - State createState() => _SettingButtonState(); -} - -class _SettingButtonState extends State { - final PopoverController _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: _popoverController, - constraints: BoxConstraints.loose(const Size(200, 400)), - 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, - ), - ), - 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 deleted file mode 100644 index 8c7d35b2e4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart +++ /dev/null @@ -1,210 +0,0 @@ -import 'dart:io'; - -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/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_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:collection/collection.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 DatabasePropertyList extends StatefulWidget { - const DatabasePropertyList({ - super.key, - required this.viewId, - required this.fieldController, - }); - - final String viewId; - final FieldController fieldController; - - @override - State createState() => _DatabasePropertyListState(); -} - -class _DatabasePropertyListState extends State { - final PopoverMutex _popoverMutex = PopoverMutex(); - late final DatabasePropertyBloc _bloc; - - @override - void initState() { - super.initState(); - _bloc = DatabasePropertyBloc( - viewId: widget.viewId, - fieldController: widget.fieldController, - )..add(const DatabasePropertyEvent.initial()); - } - - @override - void dispose() { - _popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _bloc, - child: BlocBuilder( - builder: (context, state) { - final cells = state.fieldContexts - .mapIndexed( - (index, field) => DatabasePropertyCell( - key: ValueKey(field.id), - viewId: widget.viewId, - fieldController: widget.fieldController, - fieldInfo: field, - popoverMutex: _popoverMutex, - index: index, - ), - ) - .toList(); - - return ReorderableListView( - proxyDecorator: (child, index, _) => Material( - color: Colors.transparent, - child: Stack( - children: [ - child, - MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: const SizedBox.expand(), - ), - ], - ), - ), - buildDefaultDragHandles: false, - shrinkWrap: true, - onReorder: (from, to) { - context - .read() - .add(DatabasePropertyEvent.moveField(from, to)); - }, - onReorderStart: (_) => _popoverMutex.close(), - padding: const EdgeInsets.symmetric(vertical: 4.0), - children: cells, - ); - }, - ), - ); - } -} - -@visibleForTesting -class DatabasePropertyCell extends StatefulWidget { - const DatabasePropertyCell({ - super.key, - required this.fieldInfo, - required this.viewId, - required this.popoverMutex, - required this.index, - required this.fieldController, - }); - - final FieldInfo fieldInfo; - final String viewId; - final PopoverMutex popoverMutex; - final int index; - final FieldController fieldController; - - @override - State createState() => _DatabasePropertyCellState(); -} - -class _DatabasePropertyCellState extends State { - final PopoverController _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - final visiblity = widget.fieldInfo.visibility; - final visibleIcon = FlowySvg( - visiblity != null && visiblity != FieldVisibility.AlwaysHidden - ? FlowySvgs.show_m - : FlowySvgs.hide_m, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, - ); - - return AppFlowyPopover( - mutex: widget.popoverMutex, - controller: _popoverController, - offset: const Offset(-8, 0), - direction: PopoverDirection.leftWithTopAligned, - constraints: BoxConstraints.loose(const Size(240, 400)), - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - child: Container( - height: GridSize.popoverItemHeight, - margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, - widget.fieldInfo.name, - color: AFThemeExtension.of(context).textColor, - ), - leftIconSize: const Size(36, 18), - leftIcon: Row( - children: [ - ReorderableDragStartListener( - index: widget.index, - child: MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: SizedBox( - width: 14, - height: 14, - child: FlowySvg( - FlowySvgs.drag_element_s, - color: Theme.of(context).iconTheme.color, - ), - ), - ), - ), - const HSpace(6.0), - FieldIcon( - fieldInfo: widget.fieldInfo, - ), - ], - ), - rightIcon: FlowyIconButton( - hoverColor: Colors.transparent, - onPressed: () { - if (widget.fieldInfo.fieldSettings == null) { - return; - } - - final newVisiblity = widget.fieldInfo.visibility!.toggle(); - context.read().add( - DatabasePropertyEvent.setFieldVisibility( - widget.fieldInfo.id, - newVisiblity, - ), - ); - }, - icon: visibleIcon, - ), - onTap: () => _popoverController.show(), - ), - ), - popupBuilder: (BuildContext context) { - return FieldEditor( - viewId: widget.viewId, - fieldInfo: widget.fieldInfo, - fieldController: widget.fieldController, - isNewField: false, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart deleted file mode 100644 index 7ede902085..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/share_bloc.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/string_extension.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-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 DatabaseShareButton extends StatelessWidget { - const DatabaseShareButton({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DatabaseShareBloc(view: view), - child: BlocListener( - listener: (context, state) { - state.mapOrNull( - finish: (state) { - state.successOrFail.fold( - (data) => _handleExportData(context), - _handleExportError, - ); - }, - ); - }, - child: BlocBuilder( - builder: (context, state) => IntrinsicWidth( - child: DatabaseShareActionList(view: view), - ), - ), - ), - ); - } - - void _handleExportData(BuildContext context) { - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileSuccess.tr(), - ); - } - - void _handleExportError(FlowyError error) { - showMessageToast(error.msg); - } -} - -class DatabaseShareActionList extends StatefulWidget { - const DatabaseShareActionList({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - State createState() => - DatabaseShareActionListState(); -} - -@visibleForTesting -class DatabaseShareActionListState 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 databaseShareBloc = 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), - fontSize: 14.0, - textColor: Theme.of(context).colorScheme.onPrimary, - onPressed: () {}, - ), - ), - onSelected: (action, controller) async { - switch (action.inner) { - case ShareAction.csv: - final exportPath = await getIt().saveFile( - dialogTitle: '', - fileName: '${name.toFileName()}.csv', - ); - if (exportPath != null) { - databaseShareBloc.add(DatabaseShareEvent.shareCSV(exportPath)); - } - break; - } - controller.close(); - }, - ); - } - - void listenOnViewUpdated() { - name = widget.view.name; - viewListener.start( - onViewUpdated: (view) { - name = view.name; - }, - ); - } -} - -enum ShareAction { - csv, -} - -class ShareActionWrapper extends ActionCell { - ShareActionWrapper(this.inner); - - final ShareAction inner; - - Widget? icon(Color iconColor) => null; - - @override - String get name { - switch (inner) { - case ShareAction.csv: - return LocaleKeys.shareAction_csv.tr(); - } - } -} 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 deleted file mode 100644 index ee52be8c26..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart'; -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_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:easy_localization/easy_localization.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. - -class DatabaseDocumentPage extends StatefulWidget { - const DatabaseDocumentPage({ - super.key, - required this.view, - required this.databaseId, - required this.rowId, - required this.documentId, - this.initialSelection, - }); - - final ViewPB view; - final String databaseId; - final String rowId; - final String documentId; - final Selection? initialSelection; - - @override - State createState() => _DatabaseDocumentPageState(); -} - -class _DatabaseDocumentPageState extends State { - EditorState? editorState; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value( - value: getIt(), - ), - BlocProvider( - create: (_) => DocumentBloc( - databaseViewId: widget.databaseId, - rowId: widget.rowId, - documentId: widget.documentId, - )..add(const DocumentEvent.initial()), - ), - BlocProvider( - create: (_) => - ViewBloc(view: widget.view)..add(const ViewEvent.initial()), - ), - ], - child: BlocBuilder( - 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, - ), - ); - } - - if (state.forceClose) { - return const SizedBox.shrink(); - } - - return BlocListener( - listener: _onNotificationAction, - listenWhen: (_, curr) => curr.action != null, - child: AiWriterScrollWrapper( - viewId: widget.view.id, - editorState: editorState, - child: _buildEditorPage(context, state), - ), - ); - }, - ), - ); - } - - Widget _buildEditorPage(BuildContext context, DocumentState state) { - final appflowyEditorPage = EditorDropHandler( - viewId: widget.view.id, - 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() - : '', - ), - ); - - 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), - ], - ), - ), - ); - } - - Widget _buildDatabaseDataContent( - BuildContext context, - EditorState editorState, - ) { - return BlocProvider( - create: (context) => RelatedRowDetailPageBloc( - databaseId: widget.databaseId, - initialRowId: widget.rowId, - ), - child: BlocBuilder( - builder: (context, 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( - viewId: databaseController.viewId, - fieldController: databaseController.fieldController, - cellBuilder: EditableCellBuilder( - databaseController: databaseController, - ), - ), - ), - const TypeOptionSeparator(spacing: 24.0), - ], - ), - ); - }, - ); - }, - ), - ); - } - - Widget _buildBanner(BuildContext context) { - return DocumentBanner( - viewName: widget.view.name, - onRestore: () => context.read().add( - const DocumentEvent.restorePage(), - ), - onDelete: () => context.read().add( - const DocumentEvent.deletePermanently(), - ), - ); - } - - void _onNotificationAction( - BuildContext context, - ActionNavigationState state, - ) { - if (state.action != null && state.action!.type == ActionType.jumpToBlock) { - final path = state.action?.arguments?[ActionArgumentKeys.nodePath]; - - final editorState = context.read().state.editorState; - if (editorState != null && widget.documentId == state.action?.objectId) { - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [path])), - ); - } - } - } -} 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 deleted file mode 100644 index fd238271b7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart +++ /dev/null @@ -1,143 +0,0 @@ -library; - -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'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'database_document_page.dart'; -import 'presentation/database_document_title.dart'; - -// This widget is largely copied from `plugins/document/document_plugin.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. - -class DatabaseDocumentContext { - DatabaseDocumentContext({ - required this.view, - required this.databaseId, - required this.rowId, - required this.documentId, - }); - - final ViewPB view; - final String databaseId; - final String rowId; - final String documentId; -} - -class DatabaseDocumentPluginBuilder extends PluginBuilder { - @override - Plugin build(dynamic data) { - if (data is DatabaseDocumentContext) { - return DatabaseDocumentPlugin(pluginType: pluginType, data: data); - } - - throw FlowyPluginException.invalidData; - } - - @override - String get menuName => LocaleKeys.document_menuName.tr(); - - @override - FlowySvgData get icon => FlowySvgs.icon_document_s; - - @override - PluginType get pluginType => PluginType.databaseDocument; - - @override - ViewLayoutPB get layoutType => ViewLayoutPB.Document; -} - -class DatabaseDocumentPlugin extends Plugin { - DatabaseDocumentPlugin({ - required this.data, - required PluginType pluginType, - this.initialSelection, - }) : _pluginType = pluginType; - - final DatabaseDocumentContext data; - final PluginType _pluginType; - - final Selection? initialSelection; - - @override - PluginWidgetBuilder get widgetBuilder => DatabaseDocumentPluginWidgetBuilder( - view: data.view, - databaseId: data.databaseId, - rowId: data.rowId, - documentId: data.documentId, - initialSelection: initialSelection, - ); - - @override - PluginType get pluginType => _pluginType; - - @override - PluginId get id => data.rowId; -} - -class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder - with NavigationItem { - DatabaseDocumentPluginWidgetBuilder({ - required this.view, - required this.databaseId, - required this.rowId, - required this.documentId, - this.initialSelection, - }); - - final ViewPB view; - final String databaseId; - final String rowId; - final String documentId; - final Selection? initialSelection; - - @override - String? get viewName => view.nameOrDefault; - - @override - EdgeInsets get contentPadding => EdgeInsets.zero; - - @override - Widget buildWidget({ - required PluginContext context, - required bool shrinkWrap, - Map? data, - }) { - return BlocBuilder( - builder: (_, state) => DatabaseDocumentPage( - key: ValueKey(documentId), - view: view, - databaseId: databaseId, - documentId: documentId, - rowId: rowId, - initialSelection: initialSelection, - ), - ); - } - - @override - Widget get leftBarItem => - ViewTitleBarWithRow(view: view, databaseId: databaseId, rowId: rowId); - - @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => - const SizedBox.shrink(); - - @override - Widget? get rightBarItem => const SizedBox.shrink(); - - @override - List get navigationItems => [this]; -} - -class DatabaseDocumentPluginConfig implements PluginConfig { - @override - bool get creatable => false; -} 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 deleted file mode 100644 index 7f4493a999..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart +++ /dev/null @@ -1,277 +0,0 @@ -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/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_backend/protobuf/flowy-folder/view.pb.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'; - -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. - -// workspaces / ... / database view name / row name -class ViewTitleBarWithRow extends StatelessWidget { - const ViewTitleBarWithRow({ - super.key, - required this.view, - required this.databaseId, - required this.rowId, - }); - - final ViewPB view; - final String databaseId; - final String rowId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DatabaseDocumentTitleBloc( - view: view, - rowId: rowId, - ), - child: BlocBuilder( - builder: (context, state) { - 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), - ), - ), - ); - }, - ), - ); - } - - List _buildViewTitles(List views) { - // 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.last), - const FlowySvg(FlowySvgs.title_bar_divider_s), - _buildRowName(), - ] - : [ - ...views - .map( - (e) => [ - _buildViewButton(e), - const FlowySvg(FlowySvgs.title_bar_divider_s), - ], - ) - .flattened, - _buildRowName(), - ]; - } - - Widget _buildViewButton(ViewPB view) { - return FlowyTooltip( - message: view.name, - child: ViewTitle( - view: view, - behavior: ViewTitleBehavior.uneditable, - onUpdated: () {}, - ), - ); - } - - Widget _buildRowName() { - return _RowName( - rowId: rowId, - ); - } -} - -class _RowName extends StatelessWidget { - const _RowName({ - required this.rowId, - }); - - final String rowId; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.databaseController == null) { - return const SizedBox.shrink(); - } - - final cellBuilder = EditableCellBuilder( - databaseController: state.databaseController!, - ); - - return cellBuilder.buildCustom( - CellContext( - fieldId: state.fieldId!, - rowId: rowId, - ), - skinMap: EditableCellSkinMap(textSkin: _TitleSkin()), - ); - }, - ); - } -} - -class _TitleSkin extends IEditableTextCellSkin { - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TextCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return BlocSelector( - selector: (state) => state.content ?? "", - builder: (context, content) { - final name = content.isEmpty - ? LocaleKeys.grid_row_titlePlaceholder.tr() - : content; - return BlocBuilder( - builder: (context, state) { - return FlowyTooltip( - message: name, - child: AppFlowyPopover( - constraints: const BoxConstraints( - maxWidth: 300, - maxHeight: 44, - ), - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 18), - popupBuilder: (_) { - return RenameRowPopover( - textController: textEditingController, - icon: state.icon ?? EmojiIconData.none(), - onUpdateIcon: (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), - ], - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 180), - child: FlowyText.regular( - name, - overflow: TextOverflow.ellipsis, - fontSize: 14.0, - figmaLineHeight: 18.0, - ), - ), - ], - ), - ), - ), - ); - }, - ); - }, - ); - } -} - -class RenameRowPopover extends StatefulWidget { - const RenameRowPopover({ - super.key, - required this.textController, - required this.onUpdateName, - required this.onUpdateIcon, - required this.icon, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], - }); - - final TextEditingController textController; - final EmojiIconData icon; - - final ValueChanged onUpdateName; - final ValueChanged onUpdateIcon; - final List tabs; - - @override - State createState() => _RenameRowPopoverState(); -} - -class _RenameRowPopoverState extends State { - @override - void initState() { - super.initState(); - widget.textController.selection = TextSelection( - baseOffset: 0, - extentOffset: widget.textController.value.text.characters.length, - ); - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - EmojiPickerButton( - emoji: widget.icon, - 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(); - }, - tabs: widget.tabs, - ), - const HSpace(6), - SizedBox( - height: 36.0, - width: 220, - child: FlowyTextField( - controller: widget.textController, - maxLength: 256, - onSubmitted: (text) { - widget.onUpdateName(text); - PopoverContainer.of(context).close(); - }, - onCanceled: () => widget.onUpdateName(widget.textController.text), - showCounter: false, - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index 2711274cb2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart +++ /dev/null @@ -1,183 +0,0 @@ -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'; -import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy/plugins/database/domain/row_meta_listener.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_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 - extends Bloc { - DatabaseDocumentTitleBloc({ - required this.view, - required this.rowId, - }) : _metaListener = RowMetaListener(rowId), - super(DatabaseDocumentTitleState.initial()) { - _dispatch(); - _startListening(); - _init(); - } - - final ViewPB view; - final String rowId; - final RowMetaListener _metaListener; - - void _dispatch() { - on((event, emit) async { - event.when( - didUpdateAncestors: (ancestors) { - emit( - state.copyWith( - ancestors: ancestors, - ), - ); - }, - didUpdateRowTitleInfo: (databaseController, rowController, fieldId) { - emit( - state.copyWith( - databaseController: databaseController, - rowController: rowController, - fieldId: fieldId, - ), - ); - }, - didUpdateRowIcon: (icon) { - emit( - state.copyWith( - icon: icon, - ), - ); - }, - updateIcon: (icon) { - _updateMeta(icon.emoji); - }, - ); - }); - } - - void _startListening() { - _metaListener.start( - callback: (rowMeta) { - if (!isClosed) { - add( - DatabaseDocumentTitleEvent.didUpdateRowIcon( - EmojiIconData.emoji(rowMeta.icon), - ), - ); - } - }, - ); - } - - void _init() async { - // get the database controller, row controller and primary field id - final databaseController = DatabaseController(view: view); - await databaseController.open().fold( - (s) => databaseController.setIsLoading(false), - (f) => null, - ); - final rowInfo = databaseController.rowCache.getRow(rowId); - if (rowInfo == null) { - return; - } - final rowController = RowController( - rowMeta: rowInfo.rowMeta, - viewId: view.id, - rowCache: databaseController.rowCache, - ); - unawaited(rowController.initialize()); - - final primaryFieldId = - await FieldBackendService.getPrimaryField(viewId: view.id).fold( - (primaryField) => primaryField.id, - (r) { - Log.error(r); - return null; - }, - ); - if (primaryFieldId != null) { - add( - DatabaseDocumentTitleEvent.didUpdateRowTitleInfo( - databaseController, - rowController, - primaryFieldId, - ), - ); - } - - // load ancestors - final ancestors = await ViewBackendService.getViewAncestors(view.id) - .fold((s) => s.items, (f) => []); - add(DatabaseDocumentTitleEvent.didUpdateAncestors(ancestors)); - - // initialize icon - if (rowInfo.rowMeta.icon.isNotEmpty) { - add( - DatabaseDocumentTitleEvent.didUpdateRowIcon( - EmojiIconData.emoji(rowInfo.rowMeta.icon), - ), - ); - } - } - - /// Update the meta of the row and the view - void _updateMeta(String iconURL) { - RowBackendService(viewId: view.id) - .updateMeta( - iconURL: iconURL, - rowId: rowId, - ) - .fold((l) => null, (err) => Log.error(err)); - } -} - -@freezed -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, - ) = _DidUpdateRowIcon; - - const factory DatabaseDocumentTitleEvent.updateIcon( - EmojiIconData icon, - ) = _UpdateIcon; -} - -@freezed -class DatabaseDocumentTitleState with _$DatabaseDocumentTitleState { - const factory DatabaseDocumentTitleState({ - required List ancestors, - required DatabaseController? databaseController, - required RowController? rowController, - required String? fieldId, - required EmojiIconData? icon, - }) = _DatabaseDocumentTitleState; - - factory DatabaseDocumentTitleState.initial() => - const DatabaseDocumentTitleState( - ancestors: [], - databaseController: null, - rowController: null, - fieldId: null, - icon: null, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart new file mode 100644 index 0000000000..f66fe1f3bb --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart @@ -0,0 +1,77 @@ +part of 'cell_service.dart'; + +typedef CellContextByFieldId = LinkedHashMap; + +class DatabaseCell { + dynamic object; + DatabaseCell({ + required this.object, + }); +} + +/// Use to index the cell in the grid. +/// We use [fieldId + rowId] to identify the cell. +class CellCacheKey { + final String fieldId; + final RowId rowId; + CellCacheKey({ + required this.fieldId, + required this.rowId, + }); +} + +/// GridCellCache is used to cache cell data of each block. +/// We use GridCellCacheKey to index the cell in the cache. +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid +/// for more information +class CellCache { + final String viewId; + + /// fieldId: {cacheKey: GridCell} + final Map> _cellDataByFieldId = {}; + CellCache({ + required this.viewId, + }); + + void removeCellWithFieldId(String fieldId) { + _cellDataByFieldId.remove(fieldId); + } + + void remove(CellCacheKey key) { + final map = _cellDataByFieldId[key.fieldId]; + if (map != null) { + map.remove(key.rowId); + } + } + + void insert(CellCacheKey key, T value) { + var map = _cellDataByFieldId[key.fieldId]; + if (map == null) { + _cellDataByFieldId[key.fieldId] = {}; + map = _cellDataByFieldId[key.fieldId]; + } + + map![key.rowId] = value.object; + } + + T? get(CellCacheKey key) { + final map = _cellDataByFieldId[key.fieldId]; + if (map == null) { + return null; + } else { + final value = map[key.rowId]; + if (value is T) { + return value; + } else { + if (value != null) { + Log.error("Expected value type: $T, but receive $value"); + } + return null; + } + } + } + + Future dispose() async { + _cellDataByFieldId.clear(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart new file mode 100644 index 0000000000..586734a96e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/field/field_listener.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import '../field/field_controller.dart'; +import '../field/field_service.dart'; +import '../field/type_option/type_option_context.dart'; +import 'cell_listener.dart'; +import 'cell_service.dart'; + +/// IGridCellController is used to manipulate the cell and receive notifications. +/// * Read/Write cell data +/// * Listen on field/cell notifications. +/// +/// Generic T represents the type of the cell data. +/// Generic D represents the type of data that will be saved to the disk +/// +// ignore: must_be_immutable +class CellController extends Equatable { + DatabaseCellContext _cellContext; + final CellCache _cellCache; + final CellCacheKey _cacheKey; + final FieldBackendService _fieldBackendSvc; + final CellDataLoader _cellDataLoader; + final CellDataPersistence _cellDataPersistence; + + CellListener? _cellListener; + RowMetaListener? _rowMetaListener; + SingleFieldListener? _fieldListener; + CellDataNotifier? _cellDataNotifier; + + VoidCallback? _onCellFieldChanged; + VoidCallback? _onRowMetaChanged; + Timer? _loadDataOperation; + Timer? _saveDataOperation; + + String get viewId => _cellContext.viewId; + + RowId get rowId => _cellContext.rowId; + + String get fieldId => _cellContext.fieldInfo.id; + + FieldInfo get fieldInfo => _cellContext.fieldInfo; + + FieldType get fieldType => _cellContext.fieldInfo.fieldType; + + String? get emoji => _cellContext.emoji; + + CellController({ + required DatabaseCellContext cellContext, + required CellCache cellCache, + required CellDataLoader cellDataLoader, + required CellDataPersistence cellDataPersistence, + }) : _cellContext = cellContext, + _cellCache = cellCache, + _cellDataLoader = cellDataLoader, + _cellDataPersistence = cellDataPersistence, + _rowMetaListener = RowMetaListener(cellContext.rowId), + _fieldListener = SingleFieldListener(fieldId: cellContext.fieldId), + _fieldBackendSvc = FieldBackendService( + viewId: cellContext.viewId, + fieldId: cellContext.fieldInfo.id, + ), + _cacheKey = CellCacheKey( + rowId: cellContext.rowId, + fieldId: cellContext.fieldInfo.id, + ) { + _cellDataNotifier = CellDataNotifier(value: _cellCache.get(_cacheKey)); + _cellListener = CellListener( + rowId: cellContext.rowId, + fieldId: cellContext.fieldInfo.id, + ); + + /// 1.Listen on user edit event and load the new cell data if needed. + /// For example: + /// user input: 12 + /// cell display: $12 + _cellListener?.start( + onCellChanged: (result) { + result.fold( + (_) => _loadData(), + (err) => Log.error(err), + ); + }, + ); + + /// 2.Listen on the field event and load the cell data if needed. + _fieldListener?.start( + onFieldChanged: (fieldPB) { + /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed + /// For example: + /// ¥12 -> $12 + if (_cellDataLoader.reloadOnFieldChanged) { + _loadData(); + } + _onCellFieldChanged?.call(); + }, + ); + + _rowMetaListener?.start( + callback: (newRowMeta) { + _cellContext = _cellContext.copyWith(rowMeta: newRowMeta); + _onRowMetaChanged?.call(); + }, + ); + } + + /// Listen on the cell content or field changes + VoidCallback? startListening({ + required void Function(T?) onCellChanged, + VoidCallback? onRowMetaChanged, + VoidCallback? onCellFieldChanged, + }) { + _onCellFieldChanged = onCellFieldChanged; + _onRowMetaChanged = onRowMetaChanged; + + /// Notify the listener, the cell data was changed. + onCellChangedFn() => onCellChanged(_cellDataNotifier?.value); + _cellDataNotifier?.addListener(onCellChangedFn); + + // Return the function pointer that can be used when calling removeListener. + return onCellChangedFn; + } + + void removeListener(VoidCallback fn) { + _cellDataNotifier?.removeListener(fn); + } + + /// Return the cell data. + /// The cell data will be read from the Cache first, and load from disk if it does not exist. + /// You can set [loadIfNotExist] to false (default is true) to disable loading the cell data. + T? getCellData({bool loadIfNotExist = true}) { + final data = _cellCache.get(_cacheKey); + if (data == null && loadIfNotExist) { + _loadData(); + } + return data; + } + + /// Return the TypeOptionPB that can be parsed into corresponding class using the [parser]. + /// [PD] is the type that the parser return. + Future> getTypeOption( + P parser, + ) { + return _fieldBackendSvc + .getFieldTypeOptionData(fieldType: fieldType) + .then((result) { + return result.fold( + (data) => left(parser.fromBuffer(data.typeOptionData)), + (err) => right(err), + ); + }); + } + + /// Save the cell data to disk + /// You can set [deduplicate] to true (default is false) to reduce the save operation. + /// It's useful when you call this method when user editing the [TextField]. + /// The default debounce interval is 300 milliseconds. + Future saveCellData( + D data, { + bool deduplicate = false, + void Function(Option)? onFinish, + }) async { + _loadDataOperation?.cancel(); + if (deduplicate) { + _saveDataOperation?.cancel(); + _saveDataOperation = Timer(const Duration(milliseconds: 300), () async { + final result = await _cellDataPersistence.save(data); + onFinish?.call(result); + }); + } else { + final result = await _cellDataPersistence.save(data); + onFinish?.call(result); + } + } + + void _loadData() { + _saveDataOperation?.cancel(); + _loadDataOperation?.cancel(); + + _loadDataOperation = Timer(const Duration(milliseconds: 10), () { + _cellDataLoader.loadData().then((data) { + if (data != null) { + _cellCache.insert(_cacheKey, DatabaseCell(object: data)); + } else { + _cellCache.remove(_cacheKey); + } + _cellDataNotifier?.value = data; + }); + }); + } + + Future dispose() async { + await _rowMetaListener?.stop(); + _rowMetaListener = null; + + await _cellListener?.stop(); + _cellListener = null; + + await _fieldListener?.stop(); + _fieldListener = null; + + _loadDataOperation?.cancel(); + _saveDataOperation?.cancel(); + _cellDataNotifier?.dispose(); + _cellDataNotifier = null; + _onRowMetaChanged = null; + } + + @override + List get props => [ + _cellCache.get(_cacheKey) ?? "", + _cellContext.rowId + _cellContext.fieldInfo.id + ]; +} + +class CellDataNotifier extends ChangeNotifier { + T _value; + bool Function(T? oldValue, T? newValue)? listenWhen; + CellDataNotifier({required T value, this.listenWhen}) : _value = value; + + set value(T newValue) { + if (listenWhen?.call(_value, newValue) ?? false) { + _value = newValue; + notifyListeners(); + } else { + if (_value != newValue) { + _value = newValue; + notifyListeners(); + } + } + } + + T get value => _value; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart new file mode 100644 index 0000000000..70fee21e35 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart @@ -0,0 +1,129 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; + +import 'cell_controller.dart'; +import 'cell_service.dart'; + +typedef TextCellController = CellController; +typedef CheckboxCellController = CellController; +typedef NumberCellController = CellController; +typedef SelectOptionCellController + = CellController; +typedef ChecklistCellController = CellController; +typedef DateCellController = CellController; +typedef URLCellController = CellController; + +class CellControllerBuilder { + final DatabaseCellContext _cellContext; + final CellCache _cellCache; + + CellControllerBuilder({ + required DatabaseCellContext cellContext, + required CellCache cellCache, + }) : _cellCache = cellCache, + _cellContext = cellContext; + + CellController build() { + switch (_cellContext.fieldType) { + case FieldType.Checkbox: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: StringCellDataParser(), + ); + return TextCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + TextCellDataPersistence(cellContext: _cellContext), + ); + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: DateCellDataParser(), + reloadOnFieldChanged: true, + ); + + return DateCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + DateCellDataPersistence(cellContext: _cellContext), + ); + case FieldType.Number: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: NumberCellDataParser(), + reloadOnFieldChanged: true, + ); + return NumberCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + TextCellDataPersistence(cellContext: _cellContext), + ); + case FieldType.RichText: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: StringCellDataParser(), + ); + return TextCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + TextCellDataPersistence(cellContext: _cellContext), + ); + case FieldType.MultiSelect: + case FieldType.SingleSelect: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: SelectOptionCellDataParser(), + reloadOnFieldChanged: true, + ); + + return SelectOptionCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + TextCellDataPersistence(cellContext: _cellContext), + ); + + case FieldType.Checklist: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: ChecklistCellDataParser(), + reloadOnFieldChanged: true, + ); + + return ChecklistCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + TextCellDataPersistence(cellContext: _cellContext), + ); + case FieldType.URL: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: URLCellDataParser(), + ); + return URLCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + TextCellDataPersistence(cellContext: _cellContext), + ); + } + throw UnimplementedError; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart new file mode 100644 index 0000000000..f8be693e6b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart @@ -0,0 +1,105 @@ +part of 'cell_service.dart'; + +abstract class IGridCellDataConfig { + // The cell data will reload if it receives the field's change notification. + bool get reloadOnFieldChanged; +} + +abstract class CellDataParser { + T? parserData(List data); +} + +class CellDataLoader { + final CellBackendService service = CellBackendService(); + final DatabaseCellContext cellContext; + final CellDataParser parser; + final bool reloadOnFieldChanged; + + CellDataLoader({ + required this.cellContext, + required this.parser, + this.reloadOnFieldChanged = false, + }); + + Future loadData() { + final fut = service.getCell(cellContext: cellContext); + return fut.then( + (result) => result.fold( + (CellPB cell) { + try { + // Return null the data of the cell is empty. + if (cell.data.isEmpty) { + return null; + } else { + return parser.parserData(cell.data); + } + } catch (e, s) { + Log.error('$parser parser cellData failed, $e'); + Log.error('Stack trace \n $s'); + return null; + } + }, + (err) { + Log.error(err); + return null; + }, + ), + ); + } +} + +class StringCellDataParser implements CellDataParser { + @override + String? parserData(List data) { + final s = utf8.decode(data); + return s; + } +} + +class NumberCellDataParser implements CellDataParser { + @override + String? parserData(List data) { + return utf8.decode(data); + } +} + +class DateCellDataParser implements CellDataParser { + @override + DateCellDataPB? parserData(List data) { + if (data.isEmpty) { + return null; + } + return DateCellDataPB.fromBuffer(data); + } +} + +class SelectOptionCellDataParser + implements CellDataParser { + @override + SelectOptionCellDataPB? parserData(List data) { + if (data.isEmpty) { + return null; + } + return SelectOptionCellDataPB.fromBuffer(data); + } +} + +class ChecklistCellDataParser implements CellDataParser { + @override + ChecklistCellDataPB? parserData(List data) { + if (data.isEmpty) { + return null; + } + return ChecklistCellDataPB.fromBuffer(data); + } +} + +class URLCellDataParser implements CellDataParser { + @override + URLCellDataPB? parserData(List data) { + if (data.isEmpty) { + return null; + } + return URLCellDataPB.fromBuffer(data); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart new file mode 100644 index 0000000000..001e1e4dac --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart @@ -0,0 +1,74 @@ +part of 'cell_service.dart'; + +/// Save the cell data to disk +/// You can extend this class to do custom operations. For example, the DateCellDataPersistence. +abstract class CellDataPersistence { + Future> save(D data); +} + +class TextCellDataPersistence implements CellDataPersistence { + final DatabaseCellContext cellContext; + final _cellBackendSvc = CellBackendService(); + + TextCellDataPersistence({ + required this.cellContext, + }); + + @override + Future> save(String data) async { + final fut = _cellBackendSvc.updateCell( + cellContext: cellContext, + data: data, + ); + return fut.then((result) { + return result.fold( + (l) => none(), + (err) => Some(err), + ); + }); + } +} + +@freezed +class DateCellData with _$DateCellData { + const factory DateCellData({ + DateTime? dateTime, + String? time, + required bool includeTime, + }) = _DateCellData; +} + +class DateCellDataPersistence implements CellDataPersistence { + final DatabaseCellContext cellContext; + DateCellDataPersistence({ + required this.cellContext, + }); + + @override + Future> save(DateCellData data) { + final payload = DateChangesetPB.create() + ..cellId = _makeCellPath(cellContext); + if (data.dateTime != null) { + final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString(); + payload.date = date; + } + if (data.time != null) { + payload.time = data.time!; + } + payload.includeTime = data.includeTime; + + return DatabaseEventUpdateDateCell(payload).send().then((result) { + return result.fold( + (l) => none(), + (err) => Some(err), + ); + }); + } +} + +CellIdPB _makeCellPath(DatabaseCellContext cellId) { + return CellIdPB.create() + ..viewId = cellId.viewId + ..fieldId = cellId.fieldId + ..rowId = cellId.rowId; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_listener.dart new file mode 100644 index 0000000000..be015b81bd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_listener.dart @@ -0,0 +1,47 @@ +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'dart:async'; +import 'dart:typed_data'; + +import '../row/row_service.dart'; + +typedef UpdateFieldNotifiedValue = Either; + +class CellListener { + final RowId rowId; + final String fieldId; + PublishNotifier? _updateCellNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + CellListener({required this.rowId, required this.fieldId}); + + void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) { + _updateCellNotifier?.addPublishListener(onCellChanged); + _listener = DatabaseNotificationListener( + objectId: "$rowId:$fieldId", + handler: _handler, + ); + } + + void _handler(DatabaseNotification ty, Either result) { + switch (ty) { + case DatabaseNotification.DidUpdateCell: + result.fold( + (payload) => _updateCellNotifier?.value = left(unit), + (error) => _updateCellNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _updateCellNotifier?.dispose(); + _updateCellNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart new file mode 100644 index 0000000000..31e46a5189 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; +import 'package:dartz/dartz.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-database2/cell_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:convert' show utf8; + +import '../field/field_controller.dart'; +import '../row/row_service.dart'; +part 'cell_service.freezed.dart'; +part 'cell_data_loader.dart'; +part 'cell_cache.dart'; +part 'cell_data_persistence.dart'; + +class CellBackendService { + CellBackendService(); + + Future> updateCell({ + required DatabaseCellContext cellContext, + required String data, + }) { + final payload = CellChangesetPB.create() + ..viewId = cellContext.viewId + ..fieldId = cellContext.fieldId + ..rowId = cellContext.rowId + ..cellChangeset = data; + return DatabaseEventUpdateCell(payload).send(); + } + + Future> getCell({ + required DatabaseCellContext cellContext, + }) { + final payload = CellIdPB.create() + ..viewId = cellContext.viewId + ..fieldId = cellContext.fieldId + ..rowId = cellContext.rowId; + return DatabaseEventGetCell(payload).send(); + } +} + +/// We can locate the cell by using database + rowId + field.id. +@freezed +class DatabaseCellContext with _$DatabaseCellContext { + const factory DatabaseCellContext({ + required String viewId, + required RowMetaPB rowMeta, + required FieldInfo fieldInfo, + }) = _DatabaseCellContext; + + // ignore: unused_element + const DatabaseCellContext._(); + + String get rowId => rowMeta.id; + + String get fieldId => fieldInfo.id; + + FieldType get fieldType => fieldInfo.fieldType; + + ValueKey key() { + return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}"); + } + + /// Only the primary field can have an emoji. + String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart new file mode 100644 index 0000000000..9b38fc1b0d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart @@ -0,0 +1,75 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; + +class ChecklistCellBackendService { + final String viewId; + final String fieldId; + final String rowId; + + ChecklistCellBackendService({ + required this.viewId, + required this.fieldId, + required this.rowId, + }); + + Future> create({ + required String name, + }) { + final payload = ChecklistCellDataChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..insertOptions.add(name); + + return DatabaseEventUpdateChecklistCell(payload).send(); + } + + Future> delete({ + required List optionIds, + }) { + final payload = ChecklistCellDataChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..deleteOptionIds.addAll(optionIds); + + return DatabaseEventUpdateChecklistCell(payload).send(); + } + + Future> select({ + required String optionId, + }) { + final payload = ChecklistCellDataChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..selectedOptionIds.add(optionId); + + return DatabaseEventUpdateChecklistCell(payload).send(); + } + + Future> update({ + required SelectOptionPB option, + }) { + final payload = ChecklistCellDataChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..updateOptions.add(option); + + return DatabaseEventUpdateChecklistCell(payload).send(); + } + + Future> getCellData() { + final payload = CellIdPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; + + return DatabaseEventGetChecklistCellData(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart new file mode 100644 index 0000000000..6121bdc969 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart @@ -0,0 +1,103 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; + +class SelectOptionCellBackendService { + final String viewId; + final String fieldId; + final String rowId; + + SelectOptionCellBackendService({ + required this.viewId, + required this.fieldId, + required this.rowId, + }); + + Future> create({ + required String name, + bool isSelected = true, + }) { + return TypeOptionBackendService(viewId: viewId, fieldId: fieldId) + .newOption(name: name) + .then( + (result) { + return result.fold( + (option) { + final payload = RepeatedSelectOptionPayload.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; + + if (isSelected) { + payload.items.add(option); + } else { + payload.items.add(option); + } + return DatabaseEventInsertOrUpdateSelectOption(payload).send(); + }, + (r) => right(r), + ); + }, + ); + } + + Future> update({ + required SelectOptionPB option, + }) { + final payload = RepeatedSelectOptionPayload.create() + ..items.add(option) + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; + return DatabaseEventInsertOrUpdateSelectOption(payload).send(); + } + + Future> delete({ + required Iterable options, + }) { + final payload = RepeatedSelectOptionPayload.create() + ..items.addAll(options) + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; + + return DatabaseEventDeleteSelectOption(payload).send(); + } + + Future> getCellData() { + final payload = CellIdPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; + + return DatabaseEventGetSelectOptionCellData(payload).send(); + } + + Future> select({ + required Iterable optionIds, + }) { + final payload = SelectOptionCellChangesetPB.create() + ..cellIdentifier = _cellIdentifier() + ..insertOptionIds.addAll(optionIds); + return DatabaseEventUpdateSelectOptionCell(payload).send(); + } + + Future> unSelect({ + required Iterable optionIds, + }) { + final payload = SelectOptionCellChangesetPB.create() + ..cellIdentifier = _cellIdentifier() + ..deleteOptionIds.addAll(optionIds); + return DatabaseEventUpdateSelectOptionCell(payload).send(); + } + + CellIdPB _cellIdentifier() { + return CellIdPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart new file mode 100644 index 0000000000..218d054815 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -0,0 +1,402 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/view/view_cache.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:collection/collection.dart'; +import 'dart:async'; +import 'package:dartz/dartz.dart'; +import 'database_view_service.dart'; +import 'defines.dart'; +import 'layout/layout_service.dart'; +import 'layout/layout_setting_listener.dart'; +import 'row/row_cache.dart'; +import 'group/group_listener.dart'; +import 'row/row_service.dart'; + +typedef OnGroupByField = void Function(List); +typedef OnUpdateGroup = void Function(List); +typedef OnDeleteGroup = void Function(List); +typedef OnInsertGroup = void Function(InsertedGroupPB); + +class GroupCallbacks { + final OnGroupByField? onGroupByField; + final OnUpdateGroup? onUpdateGroup; + final OnDeleteGroup? onDeleteGroup; + final OnInsertGroup? onInsertGroup; + + GroupCallbacks({ + this.onGroupByField, + this.onUpdateGroup, + this.onDeleteGroup, + this.onInsertGroup, + }); +} + +class DatabaseLayoutSettingCallbacks { + final void Function(DatabaseLayoutSettingPB) onLayoutChanged; + final void Function(DatabaseLayoutSettingPB) onLoadLayout; + + DatabaseLayoutSettingCallbacks({ + required this.onLayoutChanged, + required this.onLoadLayout, + }); +} + +class DatabaseCallbacks { + OnDatabaseChanged? onDatabaseChanged; + OnFieldsChanged? onFieldsChanged; + OnFiltersChanged? onFiltersChanged; + OnSortsChanged? onSortsChanged; + OnNumOfRowsChanged? onNumOfRowsChanged; + OnRowsDeleted? onRowsDeleted; + OnRowsUpdated? onRowsUpdated; + OnRowsCreated? onRowsCreated; + + DatabaseCallbacks({ + this.onDatabaseChanged, + this.onNumOfRowsChanged, + this.onFieldsChanged, + this.onFiltersChanged, + this.onSortsChanged, + this.onRowsUpdated, + this.onRowsDeleted, + this.onRowsCreated, + }); +} + +class DatabaseController { + final String viewId; + final DatabaseViewBackendService _databaseViewBackendSvc; + final FieldController fieldController; + DatabaseLayoutPB databaseLayout; + DatabaseLayoutSettingPB? databaseLayoutSetting; + late DatabaseViewCache _viewCache; + + // Callbacks + final List _databaseCallbacks = []; + final List _groupCallbacks = []; + final List _layoutCallbacks = []; + + // Getters + RowCache get rowCache => _viewCache.rowCache; + + // Listener + final DatabaseGroupListener _groupListener; + final DatabaseLayoutSettingListener _layoutListener; + + DatabaseController({required ViewPB view}) + : viewId = view.id, + _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id), + fieldController = FieldController(viewId: view.id), + _groupListener = DatabaseGroupListener(view.id), + databaseLayout = databaseLayoutFromViewLayout(view.layout), + _layoutListener = DatabaseLayoutSettingListener(view.id) { + _viewCache = DatabaseViewCache( + viewId: viewId, + fieldController: fieldController, + ); + _listenOnRowsChanged(); + _listenOnFieldsChanged(); + _listenOnGroupChanged(); + _listenOnLayoutChanged(); + } + + void addListener({ + DatabaseCallbacks? onDatabaseChanged, + DatabaseLayoutSettingCallbacks? onLayoutChanged, + GroupCallbacks? onGroupChanged, + }) { + if (onLayoutChanged != null) { + _layoutCallbacks.add(onLayoutChanged); + } + + if (onDatabaseChanged != null) { + _databaseCallbacks.add(onDatabaseChanged); + } + + if (onGroupChanged != null) { + _groupCallbacks.add(onGroupChanged); + } + } + + Future> open() async { + return _databaseViewBackendSvc.openDatabase().then((result) { + return result.fold( + (DatabasePB database) async { + databaseLayout = database.layoutType; + + // Load the actual database field data. + final fieldsOrFail = await fieldController.loadFields( + fieldIds: database.fields, + ); + return fieldsOrFail.fold( + (fields) { + // Notify the database is changed after the fields are loaded. + // The database won't can't be used until the fields are loaded. + for (final callback in _databaseCallbacks) { + callback.onDatabaseChanged?.call(database); + } + _viewCache.rowCache.setInitialRows(database.rows); + return Future(() async { + await _loadGroups(); + await _loadLayoutSetting(); + return left(fields); + }); + }, + (err) { + Log.error(err); + return right(err); + }, + ); + }, + (err) => right(err), + ); + }); + } + + Future> createRow({ + RowId? startRowId, + String? groupId, + void Function(RowDataBuilder builder)? withCells, + }) { + Map? cellDataByFieldId; + + if (withCells != null) { + final rowBuilder = RowDataBuilder(); + withCells(rowBuilder); + cellDataByFieldId = rowBuilder.build(); + } + + return _databaseViewBackendSvc.createRow( + startRowId: startRowId, + groupId: groupId, + cellDataByFieldId: cellDataByFieldId, + ); + } + + Future> moveGroupRow({ + required RowMetaPB fromRow, + required String groupId, + RowMetaPB? toRow, + }) { + return _databaseViewBackendSvc.moveGroupRow( + fromRowId: fromRow.id, + toGroupId: groupId, + toRowId: toRow?.id, + ); + } + + Future> moveRow({ + required String fromRowId, + required String toRowId, + }) { + return _databaseViewBackendSvc.moveRow( + fromRowId: fromRowId, + toRowId: toRowId, + ); + } + + Future> moveGroup({ + required String fromGroupId, + required String toGroupId, + }) { + return _databaseViewBackendSvc.moveGroup( + fromGroupId: fromGroupId, + toGroupId: toGroupId, + ); + } + + Future updateLayoutSetting( + CalendarLayoutSettingPB calendarlLayoutSetting, + ) async { + await _databaseViewBackendSvc + .updateLayoutSetting( + calendarLayoutSetting: calendarlLayoutSetting, + layoutType: databaseLayout, + ) + .then((result) { + result.fold((l) => null, (r) => Log.error(r)); + }); + } + + Future dispose() async { + await _databaseViewBackendSvc.closeView(); + await fieldController.dispose(); + await _groupListener.stop(); + await _viewCache.dispose(); + _databaseCallbacks.clear(); + _groupCallbacks.clear(); + _layoutCallbacks.clear(); + } + + Future _loadGroups() async { + final result = await _databaseViewBackendSvc.loadGroups(); + return Future( + () => result.fold( + (groups) { + for (final callback in _groupCallbacks) { + callback.onGroupByField?.call(groups.items); + } + }, + (err) => Log.error(err), + ), + ); + } + + Future _loadLayoutSetting() async { + _databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) { + result.fold( + (newDatabaseLayoutSetting) { + databaseLayoutSetting = newDatabaseLayoutSetting; + databaseLayoutSetting?.freeze(); + + for (final callback in _layoutCallbacks) { + callback.onLoadLayout(newDatabaseLayoutSetting); + } + }, + (r) => Log.error(r), + ); + }); + } + + void _listenOnRowsChanged() { + final callbacks = DatabaseViewCallbacks( + onNumOfRowsChanged: (rows, rowByRowId, reason) { + for (final callback in _databaseCallbacks) { + callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason); + } + }, + onRowsDeleted: (ids) { + for (final callback in _databaseCallbacks) { + callback.onRowsDeleted?.call(ids); + } + }, + onRowsUpdated: (ids, reason) { + for (final callback in _databaseCallbacks) { + callback.onRowsUpdated?.call(ids, reason); + } + }, + onRowsCreated: (ids) { + for (final callback in _databaseCallbacks) { + callback.onRowsCreated?.call(ids); + } + }, + ); + _viewCache.addListener(callbacks); + } + + void _listenOnFieldsChanged() { + fieldController.addListener( + onReceiveFields: (fields) { + for (final callback in _databaseCallbacks) { + callback.onFieldsChanged?.call(UnmodifiableListView(fields)); + } + }, + onSorts: (sorts) { + for (final callback in _databaseCallbacks) { + callback.onSortsChanged?.call(sorts); + } + }, + onFilters: (filters) { + for (final callback in _databaseCallbacks) { + callback.onFiltersChanged?.call(filters); + } + }, + ); + } + + void _listenOnGroupChanged() { + _groupListener.start( + onNumOfGroupsChanged: (result) { + result.fold( + (changeset) { + if (changeset.updateGroups.isNotEmpty) { + for (final callback in _groupCallbacks) { + callback.onUpdateGroup?.call(changeset.updateGroups); + } + } + + if (changeset.deletedGroups.isNotEmpty) { + for (final callback in _groupCallbacks) { + callback.onDeleteGroup?.call(changeset.deletedGroups); + } + } + + for (final insertedGroup in changeset.insertedGroups) { + for (final callback in _groupCallbacks) { + callback.onInsertGroup?.call(insertedGroup); + } + } + }, + (r) => Log.error(r), + ); + }, + onGroupByNewField: (result) { + result.fold( + (groups) { + for (final callback in _groupCallbacks) { + callback.onGroupByField?.call(groups); + } + }, + (r) => Log.error(r), + ); + }, + ); + } + + void _listenOnLayoutChanged() { + _layoutListener.start( + onLayoutChanged: (result) { + result.fold( + (newLayout) { + databaseLayoutSetting = newLayout; + databaseLayoutSetting?.freeze(); + + for (final callback in _layoutCallbacks) { + callback.onLayoutChanged(newLayout); + } + }, + (r) => Log.error(r), + ); + }, + ); + } +} + +class RowDataBuilder { + final _cellDataByFieldId = {}; + + void insertText(FieldInfo fieldInfo, String text) { + assert(fieldInfo.fieldType == FieldType.RichText); + _cellDataByFieldId[fieldInfo.field.id] = text; + } + + void insertNumber(FieldInfo fieldInfo, int num) { + assert(fieldInfo.fieldType == FieldType.Number); + _cellDataByFieldId[fieldInfo.field.id] = num.toString(); + } + + void insertDate(FieldInfo fieldInfo, DateTime date) { + assert( + [ + FieldType.DateTime, + FieldType.LastEditedTime, + FieldType.CreatedTime, + ].contains(fieldInfo.fieldType), + ); + final timestamp = date.millisecondsSinceEpoch ~/ 1000; + _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString(); + } + + Map build() { + return _cellDataByFieldId; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart new file mode 100644 index 0000000000..ebfdbdfd2d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart @@ -0,0 +1,13 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; + +class DatabaseBackendService { + static Future, FlowyError>> + getAllDatabases() { + return DatabaseEventGetDatabases().send().then((result) { + return result.fold((l) => left(l.items), (r) => right(r)); + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart new file mode 100644 index 0000000000..623f99b94d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -0,0 +1,138 @@ +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; + +class DatabaseViewBackendService { + final String viewId; + DatabaseViewBackendService({ + required this.viewId, + }); + + /// Returns the datbaase id associated with the view. + Future> getDatabaseId() async { + final payload = DatabaseViewIdPB(value: viewId); + return DatabaseEventGetDatabaseId(payload) + .send() + .then((value) => value.leftMap((l) => l.value)); + } + + Future> openDatabase() async { + final payload = DatabaseViewIdPB(value: viewId); + return DatabaseEventGetDatabase(payload).send(); + } + + Future> createRow({ + RowId? startRowId, + String? groupId, + Map? cellDataByFieldId, + }) { + final payload = CreateRowPayloadPB.create()..viewId = viewId; + payload.startRowId = startRowId ?? ""; + + if (groupId != null) { + payload.groupId = groupId; + } + + if (cellDataByFieldId != null && cellDataByFieldId.isNotEmpty) { + payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId); + } + + return DatabaseEventCreateRow(payload).send(); + } + + Future> moveGroupRow({ + required RowId fromRowId, + required String toGroupId, + RowId? toRowId, + }) { + final payload = MoveGroupRowPayloadPB.create() + ..viewId = viewId + ..fromRowId = fromRowId + ..toGroupId = toGroupId; + + if (toRowId != null) { + payload.toRowId = toRowId; + } + + return DatabaseEventMoveGroupRow(payload).send(); + } + + Future> moveRow({ + required String fromRowId, + required String toRowId, + }) { + final payload = MoveRowPayloadPB.create() + ..viewId = viewId + ..fromRowId = fromRowId + ..toRowId = toRowId; + + return DatabaseEventMoveRow(payload).send(); + } + + Future> moveGroup({ + required String fromGroupId, + required String toGroupId, + }) { + final payload = MoveGroupPayloadPB.create() + ..viewId = viewId + ..fromGroupId = fromGroupId + ..toGroupId = toGroupId; + + return DatabaseEventMoveGroup(payload).send(); + } + + Future, FlowyError>> getFields({ + List? fieldIds, + }) { + final payload = GetFieldPayloadPB.create()..viewId = viewId; + + if (fieldIds != null) { + payload.fieldIds = RepeatedFieldIdPB(items: fieldIds); + } + return DatabaseEventGetFields(payload).send().then((result) { + return result.fold((l) => left(l.items), (r) => right(r)); + }); + } + + Future> getLayoutSetting( + DatabaseLayoutPB layoutType, + ) { + final payload = DatabaseLayoutMetaPB.create() + ..viewId = viewId + ..layout = layoutType; + return DatabaseEventGetLayoutSetting(payload).send(); + } + + Future> updateLayoutSetting({ + required DatabaseLayoutPB layoutType, + CalendarLayoutSettingPB? calendarLayoutSetting, + }) { + final payload = LayoutSettingChangesetPB.create() + ..viewId = viewId + ..layoutType = layoutType; + if (calendarLayoutSetting != null) { + payload.calendar = calendarLayoutSetting; + } + + return DatabaseEventSetLayoutSetting(payload).send(); + } + + Future> closeView() { + final request = ViewIdPB(value: viewId); + return FolderEventCloseView(request).send(); + } + + Future> loadGroups() { + final payload = DatabaseViewIdPB(value: viewId); + return DatabaseEventGetGroups(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart new file mode 100644 index 0000000000..4ec8fbc0dc --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -0,0 +1,29 @@ +import 'dart:collection'; + +import 'package:appflowy/plugins/database_view/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 '../grid/presentation/widgets/filter/filter_info.dart'; +import 'field/field_controller.dart'; +import 'row/row_cache.dart'; +import 'row/row_service.dart'; + +typedef OnFieldsChanged = void Function(UnmodifiableListView); +typedef OnFiltersChanged = void Function(List); +typedef OnSortsChanged = void Function(List); +typedef OnDatabaseChanged = void Function(DatabasePB); + +typedef OnRowsCreated = void Function(List ids); +typedef OnRowsUpdated = void Function( + List ids, + RowsChangedReason reason, +); +typedef OnRowsDeleted = void Function(List ids); +typedef OnNumOfRowsChanged = void Function( + UnmodifiableListView rows, + UnmodifiableMapView rowByRowId, + RowsChangedReason reason, +); + +typedef OnError = void Function(FlowyError); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart new file mode 100644 index 0000000000..7b50c44417 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart @@ -0,0 +1,93 @@ +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'field_service.dart'; + +part 'field_action_sheet_bloc.freezed.dart'; + +class FieldActionSheetBloc + extends Bloc { + final FieldBackendService fieldService; + + FieldActionSheetBloc({required FieldContext fieldCellContext}) + : fieldService = FieldBackendService( + viewId: fieldCellContext.viewId, + fieldId: fieldCellContext.field.id, + ), + super( + FieldActionSheetState.initial( + TypeOptionPB.create()..field_2 = fieldCellContext.field, + ), + ) { + on( + (event, emit) async { + await event.map( + updateFieldName: (_UpdateFieldName value) async { + final result = await fieldService.updateField(name: value.name); + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }, + hideField: (_HideField value) async { + final result = await fieldService.updateField(visibility: false); + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }, + showField: (_ShowField value) async { + final result = await fieldService.updateField(visibility: true); + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }, + deleteField: (_DeleteField value) async { + final result = await fieldService.deleteField(); + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }, + duplicateField: (_DuplicateField value) async { + final result = await fieldService.duplicateField(); + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }, + saveField: (_SaveField value) {}, + ); + }, + ); + } +} + +@freezed +class FieldActionSheetEvent with _$FieldActionSheetEvent { + const factory FieldActionSheetEvent.updateFieldName(String name) = + _UpdateFieldName; + const factory FieldActionSheetEvent.hideField() = _HideField; + const factory FieldActionSheetEvent.showField() = _ShowField; + const factory FieldActionSheetEvent.duplicateField() = _DuplicateField; + const factory FieldActionSheetEvent.deleteField() = _DeleteField; + const factory FieldActionSheetEvent.saveField() = _SaveField; +} + +@freezed +class FieldActionSheetState with _$FieldActionSheetState { + const factory FieldActionSheetState({ + required TypeOptionPB fieldTypeOptionData, + required String errorText, + required String fieldName, + }) = _FieldActionSheetState; + + factory FieldActionSheetState.initial(TypeOptionPB data) => + FieldActionSheetState( + fieldTypeOptionData: data, + errorText: '', + fieldName: data.field_2.name, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart new file mode 100644 index 0000000000..f5b91c3a8f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart @@ -0,0 +1,87 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import 'field_listener.dart'; +import 'field_service.dart'; + +part 'field_cell_bloc.freezed.dart'; + +class FieldCellBloc extends Bloc { + final SingleFieldListener _fieldListener; + final FieldBackendService _fieldBackendSvc; + + FieldCellBloc({ + required FieldContext cellContext, + }) : _fieldListener = SingleFieldListener(fieldId: cellContext.field.id), + _fieldBackendSvc = FieldBackendService( + viewId: cellContext.viewId, + fieldId: cellContext.field.id, + ), + super(FieldCellState.initial(cellContext)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveFieldUpdate: (field) { + emit(state.copyWith(field: cellContext.field)); + }, + startUpdateWidth: (offset) { + final width = state.width + offset; + emit(state.copyWith(width: width)); + }, + endUpdateWidth: () { + if (state.width != state.field.width.toDouble()) { + _fieldBackendSvc.updateField(width: state.width); + } + }, + ); + }, + ); + } + + @override + Future close() async { + await _fieldListener.stop(); + return super.close(); + } + + void _startListening() { + _fieldListener.start( + onFieldChanged: (updatedField) { + if (isClosed) { + return; + } + add(FieldCellEvent.didReceiveFieldUpdate(updatedField)); + }, + ); + } +} + +@freezed +class FieldCellEvent with _$FieldCellEvent { + const factory FieldCellEvent.initial() = _InitialCell; + const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) = + _DidReceiveFieldUpdate; + const factory FieldCellEvent.startUpdateWidth(double offset) = + _StartUpdateWidth; + const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth; +} + +@freezed +class FieldCellState with _$FieldCellState { + const factory FieldCellState({ + required String viewId, + required FieldPB field, + required double width, + }) = _FieldCellState; + + factory FieldCellState.initial(FieldContext cellContext) => FieldCellState( + viewId: cellContext.viewId, + field: cellContext.field, + width: cellContext.field.width.toDouble(), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart new file mode 100644 index 0000000000..7d277553ed --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart @@ -0,0 +1,798 @@ +import 'dart:collection'; +import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; +import 'package:flutter/foundation.dart'; +import '../../grid/presentation/widgets/filter/filter_info.dart'; +import '../../grid/presentation/widgets/sort/sort_info.dart'; +import '../database_view_service.dart'; +import '../filter/filter_listener.dart'; +import '../filter/filter_service.dart'; +import '../row/row_cache.dart'; +import '../setting/setting_listener.dart'; +import '../setting/setting_service.dart'; +import '../sort/sort_listener.dart'; +import '../sort/sort_service.dart'; +import 'field_listener.dart'; + +class _GridFieldNotifier extends ChangeNotifier { + List _fieldInfos = []; + + set fieldInfos(List fieldInfos) { + _fieldInfos = fieldInfos; + notifyListeners(); + } + + void notify() { + notifyListeners(); + } + + UnmodifiableListView get fieldInfos => + UnmodifiableListView(_fieldInfos); +} + +class _GridFilterNotifier extends ChangeNotifier { + List _filters = []; + + set filters(List filters) { + _filters = filters; + notifyListeners(); + } + + void notify() { + notifyListeners(); + } + + List get filters => _filters; +} + +class _GridSortNotifier extends ChangeNotifier { + List _sorts = []; + + set sorts(List sorts) { + _sorts = sorts; + notifyListeners(); + } + + void notify() { + notifyListeners(); + } + + List get sorts => _sorts; +} + +typedef OnReceiveUpdateFields = void Function(List); +typedef OnReceiveFields = void Function(List); +typedef OnReceiveFilters = void Function(List); +typedef OnReceiveSorts = void Function(List); + +class FieldController { + final String viewId; + // Listeners + final FieldsListener _fieldListener; + final DatabaseSettingListener _settingListener; + final FiltersListener _filtersListener; + final SortsListener _sortsListener; + + // FFI services + final DatabaseViewBackendService _databaseViewBackendSvc; + final SettingBackendService _settingBackendSvc; + final FilterBackendService _filterBackendSvc; + final SortBackendService _sortBackendSvc; + + bool _isDisposed = false; + + // Field callbacks + final Map _fieldCallbacks = {}; + final _GridFieldNotifier _fieldNotifier = _GridFieldNotifier(); + + // Field updated callbacks + final Map)> + _updatedFieldCallbacks = {}; + + // Group callbacks + final Map _groupConfigurationByFieldId = {}; + + // Filter callbacks + final Map _filterCallbacks = {}; + _GridFilterNotifier? _filterNotifier = _GridFilterNotifier(); + final Map _filterPBByFieldId = {}; + + // Sort callbacks + final Map _sortCallbacks = {}; + _GridSortNotifier? _sortNotifier = _GridSortNotifier(); + final Map _sortPBByFieldId = {}; + + // Getters + List get fieldInfos => [..._fieldNotifier.fieldInfos]; + List get filterInfos => [..._filterNotifier?.filters ?? []]; + List get sortInfos => [..._sortNotifier?.sorts ?? []]; + + FieldInfo? getField(String fieldId) { + final fields = _fieldNotifier.fieldInfos + .where((element) => element.id == fieldId) + .toList(); + + if (fields.isEmpty) { + return null; + } + assert(fields.length == 1); + return fields.first; + } + + FilterInfo? getFilter(String filterId) { + final filters = _filterNotifier?.filters + .where((element) => element.filter.id == filterId) + .toList() ?? + []; + if (filters.isEmpty) { + return null; + } + assert(filters.length == 1); + return filters.first; + } + + SortInfo? getSort(String sortId) { + final sorts = _sortNotifier?.sorts + .where((element) => element.sortId == sortId) + .toList() ?? + []; + if (sorts.isEmpty) { + return null; + } + assert(sorts.length == 1); + return sorts.first; + } + + FieldController({required this.viewId}) + : _fieldListener = FieldsListener(viewId: viewId), + _settingListener = DatabaseSettingListener(viewId: viewId), + _filterBackendSvc = FilterBackendService(viewId: viewId), + _filtersListener = FiltersListener(viewId: viewId), + _databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId), + _sortBackendSvc = SortBackendService(viewId: viewId), + _sortsListener = SortsListener(viewId: viewId), + _settingBackendSvc = SettingBackendService(viewId: viewId) { + //Listen on field's changes + _listenOnFieldChanges(); + + //Listen on setting changes + _listenOnSettingChanges(); + + //Listen on the filter changes + _listenOnFilterChanges(); + + //Listen on the sort changes + _listenOnSortChanged(); + + _settingBackendSvc.getSetting().then((result) { + if (_isDisposed) { + return; + } + + result.fold( + (setting) => _updateSetting(setting), + (err) => Log.error(err), + ); + }); + } + + void _listenOnFilterChanges() { + //Listen on the filter changes + + deleteFilterFromChangeset( + List filters, + FilterChangesetNotificationPB changeset, + ) { + final deleteFilterIds = changeset.deleteFilters.map((e) => e.id).toList(); + if (deleteFilterIds.isNotEmpty) { + filters.retainWhere( + (element) => !deleteFilterIds.contains(element.filter.id), + ); + + _filterPBByFieldId + .removeWhere((key, value) => deleteFilterIds.contains(value.id)); + } + } + + insertFilterFromChangeset( + List filters, + FilterChangesetNotificationPB changeset, + ) { + for (final newFilter in changeset.insertFilters) { + final filterIndex = + filters.indexWhere((element) => element.filter.id == newFilter.id); + if (filterIndex == -1) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: newFilter.fieldId, + fieldType: newFilter.fieldType, + ); + if (fieldInfo != null) { + _filterPBByFieldId[fieldInfo.id] = newFilter; + filters.add(FilterInfo(viewId, newFilter, fieldInfo)); + } + } + } + } + + updateFilterFromChangeset( + List filters, + FilterChangesetNotificationPB changeset, + ) { + for (final updatedFilter in changeset.updateFilters) { + final filterIndex = filters.indexWhere( + (element) => element.filter.id == updatedFilter.filterId, + ); + // Remove the old filter + if (filterIndex != -1) { + filters.removeAt(filterIndex); + _filterPBByFieldId + .removeWhere((key, value) => value.id == updatedFilter.filterId); + } + + // Insert the filter if there is a filter and its field info is + // not null + if (updatedFilter.hasFilter()) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: updatedFilter.filter.fieldId, + fieldType: updatedFilter.filter.fieldType, + ); + + if (fieldInfo != null) { + // Insert the filter with the position: filterIndex, otherwise, + // append it to the end of the list. + final filterInfo = + FilterInfo(viewId, updatedFilter.filter, fieldInfo); + if (filterIndex != -1) { + filters.insert(filterIndex, filterInfo); + } else { + filters.add(filterInfo); + } + _filterPBByFieldId[fieldInfo.id] = updatedFilter.filter; + } + } + } + } + + _filtersListener.start( + onFilterChanged: (result) { + if (_isDisposed) { + return; + } + + result.fold( + (FilterChangesetNotificationPB changeset) { + final List filters = filterInfos; + // Deletes the filters + deleteFilterFromChangeset(filters, changeset); + + // Inserts the new filter if it's not exist + insertFilterFromChangeset(filters, changeset); + + updateFilterFromChangeset(filters, changeset); + + _updateFieldInfos(); + _filterNotifier?.filters = filters; + }, + (err) => Log.error(err), + ); + }, + ); + } + + void _listenOnSortChanged() { + deleteSortFromChangeset( + List newSortInfos, + SortChangesetNotificationPB changeset, + ) { + final deleteSortIds = changeset.deleteSorts.map((e) => e.id).toList(); + if (deleteSortIds.isNotEmpty) { + newSortInfos.retainWhere( + (element) => !deleteSortIds.contains(element.sortId), + ); + + _sortPBByFieldId + .removeWhere((key, value) => deleteSortIds.contains(value.id)); + } + } + + insertSortFromChangeset( + List newSortInfos, + SortChangesetNotificationPB changeset, + ) { + for (final newSortPB in changeset.insertSorts) { + final sortIndex = newSortInfos + .indexWhere((element) => element.sortId == newSortPB.id); + if (sortIndex == -1) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: newSortPB.fieldId, + fieldType: newSortPB.fieldType, + ); + + if (fieldInfo != null) { + _sortPBByFieldId[newSortPB.fieldId] = newSortPB; + newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo)); + } + } + } + } + + updateSortFromChangeset( + List newSortInfos, + SortChangesetNotificationPB changeset, + ) { + for (final updatedSort in changeset.updateSorts) { + final sortIndex = newSortInfos.indexWhere( + (element) => element.sortId == updatedSort.id, + ); + // Remove the old filter + if (sortIndex != -1) { + newSortInfos.removeAt(sortIndex); + } + + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: updatedSort.fieldId, + fieldType: updatedSort.fieldType, + ); + + if (fieldInfo != null) { + final newSortInfo = SortInfo( + sortPB: updatedSort, + fieldInfo: fieldInfo, + ); + if (sortIndex != -1) { + newSortInfos.insert(sortIndex, newSortInfo); + } else { + newSortInfos.add(newSortInfo); + } + _sortPBByFieldId[updatedSort.fieldId] = updatedSort; + } + } + } + + _sortsListener.start( + onSortChanged: (result) { + if (_isDisposed) { + return; + } + result.fold( + (SortChangesetNotificationPB changeset) { + final List newSortInfos = sortInfos; + deleteSortFromChangeset(newSortInfos, changeset); + insertSortFromChangeset(newSortInfos, changeset); + updateSortFromChangeset(newSortInfos, changeset); + + _updateFieldInfos(); + _sortNotifier?.sorts = newSortInfos; + }, + (err) => Log.error(err), + ); + }, + ); + } + + void _listenOnSettingChanges() { + //Listen on setting changes + _settingListener.start( + onSettingUpdated: (result) { + if (_isDisposed) { + return; + } + + result.fold( + (setting) => _updateSetting(setting), + (r) => Log.error(r), + ); + }, + ); + } + + void _listenOnFieldChanges() { + //Listen on field's changes + _fieldListener.start( + onFieldsChanged: (result) { + result.fold( + (changeset) { + if (_isDisposed) { + return; + } + _deleteFields(changeset.deletedFields); + _insertFields(changeset.insertedFields); + + final updatedFields = _updateFields(changeset.updatedFields); + for (final listener in _updatedFieldCallbacks.values) { + listener(updatedFields); + } + }, + (err) => Log.error(err), + ); + }, + ); + } + + void _updateSetting(DatabaseViewSettingPB setting) { + _groupConfigurationByFieldId.clear(); + for (final configuration in setting.groupSettings.items) { + _groupConfigurationByFieldId[configuration.fieldId] = configuration; + } + + for (final filter in setting.filters.items) { + _filterPBByFieldId[filter.fieldId] = filter; + } + + for (final sort in setting.sorts.items) { + _sortPBByFieldId[sort.fieldId] = sort; + } + + _updateFieldInfos(); + } + + void _updateFieldInfos() { + for (final field in _fieldNotifier.fieldInfos) { + field._isGroupField = _groupConfigurationByFieldId[field.id] != null; + field._hasFilter = _filterPBByFieldId[field.id] != null; + field._hasSort = _sortPBByFieldId[field.id] != null; + } + _fieldNotifier.notify(); + } + + Future dispose() async { + if (_isDisposed) { + Log.warn('FieldController is already disposed'); + return; + } + _isDisposed = true; + await _fieldListener.stop(); + await _filtersListener.stop(); + await _settingListener.stop(); + await _sortsListener.stop(); + + for (final callback in _fieldCallbacks.values) { + _fieldNotifier.removeListener(callback); + } + _fieldNotifier.dispose(); + + for (final callback in _filterCallbacks.values) { + _filterNotifier?.removeListener(callback); + } + for (final callback in _sortCallbacks.values) { + _sortNotifier?.removeListener(callback); + } + + _filterNotifier?.dispose(); + _filterNotifier = null; + + _sortNotifier?.dispose(); + _sortNotifier = null; + } + + Future> loadFields({ + required List fieldIds, + }) async { + final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds); + return Future( + () => result.fold( + (newFields) { + if (_isDisposed) { + return left(unit); + } + + _fieldNotifier.fieldInfos = + newFields.map((field) => FieldInfo(field: field)).toList(); + _loadFilters(); + _loadSorts(); + _updateFieldInfos(); + return left(unit); + }, + (err) => right(err), + ), + ); + } + + Future> _loadFilters() async { + return _filterBackendSvc.getAllFilters().then((result) { + return result.fold( + (filterPBs) { + final List filters = []; + for (final filterPB in filterPBs) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: filterPB.fieldId, + fieldType: filterPB.fieldType, + ); + if (fieldInfo != null) { + final filterInfo = FilterInfo(viewId, filterPB, fieldInfo); + filters.add(filterInfo); + } + } + + _filterNotifier?.filters = filters; + return left(unit); + }, + (err) => right(err), + ); + }); + } + + Future> _loadSorts() async { + return _sortBackendSvc.getAllSorts().then((result) { + return result.fold( + (sortPBs) { + final List sortInfos = []; + for (final sortPB in sortPBs) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: sortPB.fieldId, + fieldType: sortPB.fieldType, + ); + + if (fieldInfo != null) { + final sortInfo = SortInfo(sortPB: sortPB, fieldInfo: fieldInfo); + sortInfos.add(sortInfo); + } + } + + _updateFieldInfos(); + _sortNotifier?.sorts = sortInfos; + return left(unit); + }, + (err) => right(err), + ); + }); + } + + void addListener({ + OnReceiveFields? onReceiveFields, + OnReceiveUpdateFields? onFieldsChanged, + OnReceiveFilters? onFilters, + OnReceiveSorts? onSorts, + bool Function()? listenWhen, + }) { + if (onFieldsChanged != null) { + callback(List updateFields) { + if (listenWhen != null && listenWhen() == false) { + return; + } + onFieldsChanged(updateFields); + } + + _updatedFieldCallbacks[onFieldsChanged] = callback; + } + + if (onReceiveFields != null) { + callback() { + if (listenWhen != null && listenWhen() == false) { + return; + } + onReceiveFields(fieldInfos); + } + + _fieldCallbacks[onReceiveFields] = callback; + _fieldNotifier.addListener(callback); + } + + if (onFilters != null) { + callback() { + if (listenWhen != null && listenWhen() == false) { + return; + } + onFilters(filterInfos); + } + + _filterCallbacks[onFilters] = callback; + _filterNotifier?.addListener(callback); + } + + if (onSorts != null) { + callback() { + if (listenWhen != null && listenWhen() == false) { + return; + } + onSorts(sortInfos); + } + + _sortCallbacks[onSorts] = callback; + _sortNotifier?.addListener(callback); + } + } + + void removeListener({ + OnReceiveFields? onFieldsListener, + OnReceiveSorts? onSortsListener, + OnReceiveFilters? onFiltersListener, + OnReceiveUpdateFields? onChangesetListener, + }) { + if (onFieldsListener != null) { + final callback = _fieldCallbacks.remove(onFieldsListener); + if (callback != null) { + _fieldNotifier.removeListener(callback); + } + } + if (onFiltersListener != null) { + final callback = _filterCallbacks.remove(onFiltersListener); + if (callback != null) { + _filterNotifier?.removeListener(callback); + } + } + + if (onSortsListener != null) { + final callback = _sortCallbacks.remove(onSortsListener); + if (callback != null) { + _sortNotifier?.removeListener(callback); + } + } + } + + void _deleteFields(List deletedFields) { + if (deletedFields.isEmpty) { + return; + } + final List newFields = fieldInfos; + final Map deletedFieldMap = { + for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder + }; + + newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); + _fieldNotifier.fieldInfos = newFields; + } + + void _insertFields(List insertedFields) { + if (insertedFields.isEmpty) { + return; + } + final List newFieldInfos = fieldInfos; + for (final indexField in insertedFields) { + final fieldInfo = FieldInfo(field: indexField.field_1); + if (newFieldInfos.length > indexField.index) { + newFieldInfos.insert(indexField.index, fieldInfo); + } else { + newFieldInfos.add(fieldInfo); + } + } + _fieldNotifier.fieldInfos = newFieldInfos; + } + + List _updateFields(List updatedFieldPBs) { + if (updatedFieldPBs.isEmpty) { + return []; + } + + final List newFields = fieldInfos; + final List updatedFields = []; + for (final updatedFieldPB in updatedFieldPBs) { + final index = + newFields.indexWhere((field) => field.id == updatedFieldPB.id); + if (index != -1) { + newFields.removeAt(index); + final fieldInfo = FieldInfo(field: updatedFieldPB); + newFields.insert(index, fieldInfo); + updatedFields.add(fieldInfo); + } + } + + if (updatedFields.isNotEmpty) { + _fieldNotifier.fieldInfos = newFields; + } + return updatedFields; + } +} + +class RowDelegatesImpl extends RowFieldsDelegate with RowCacheDelegate { + final FieldController _cache; + OnReceiveFields? _onFieldFn; + RowDelegatesImpl(FieldController cache) : _cache = cache; + + @override + UnmodifiableListView get fields => + UnmodifiableListView(_cache.fieldInfos); + + @override + void onFieldsChanged(void Function(List) callback) { + _onFieldFn = (fieldInfos) { + callback(fieldInfos); + }; + _cache.addListener(onReceiveFields: _onFieldFn); + } + + @override + void onRowDispose() { + if (_onFieldFn != null) { + _cache.removeListener(onFieldsListener: _onFieldFn!); + _onFieldFn = null; + } + } +} + +FieldInfo? _findFieldInfo({ + required List fieldInfos, + required String fieldId, + required FieldType fieldType, +}) { + final fieldIndex = fieldInfos.indexWhere((element) { + return element.id == fieldId && element.fieldType == fieldType; + }); + if (fieldIndex != -1) { + return fieldInfos[fieldIndex]; + } else { + return null; + } +} + +class FieldInfo { + final FieldPB _field; + bool _isGroupField = false; + + bool _hasFilter = false; + + bool _hasSort = false; + + String get id => _field.id; + + FieldType get fieldType => _field.fieldType; + + bool get visibility => _field.visibility; + + double get width => _field.width.toDouble(); + + bool get isPrimary => _field.isPrimary; + + String get name => _field.name; + + FieldPB get field => _field; + + bool get isGroupField => _isGroupField; + + bool get hasFilter => _hasFilter; + + bool get canBeGroup { + switch (_field.fieldType) { + case FieldType.URL: + case FieldType.Checkbox: + case FieldType.MultiSelect: + case FieldType.SingleSelect: + return true; + default: + return false; + } + } + + bool get canCreateFilter { + if (hasFilter) return false; + + switch (_field.fieldType) { + case FieldType.Checkbox: + case FieldType.MultiSelect: + case FieldType.RichText: + case FieldType.SingleSelect: + case FieldType.Checklist: + return true; + default: + return false; + } + } + + bool get canCreateSort { + if (_hasSort) return false; + + switch (_field.fieldType) { + case FieldType.RichText: + case FieldType.Checkbox: + case FieldType.Number: + return true; + default: + return false; + } + } + + FieldInfo({required FieldPB field}) : _field = field; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart new file mode 100644 index 0000000000..32cada4ff1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart @@ -0,0 +1,112 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'field_service.dart'; +import 'type_option/type_option_context.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'type_option/type_option_data_controller.dart'; + +part 'field_editor_bloc.freezed.dart'; + +class FieldEditorBloc extends Bloc { + final TypeOptionController dataController; + + FieldEditorBloc({ + required bool isGroupField, + required FieldPB field, + required FieldTypeOptionLoader loader, + }) : dataController = TypeOptionController( + field: field, + loader: loader, + ), + super( + FieldEditorState.initial( + loader.viewId, + loader.field.name, + isGroupField, + ), + ) { + on( + (event, emit) async { + await event.when( + initial: () async { + dataController.addFieldListener((field) { + if (!isClosed) { + add(FieldEditorEvent.didReceiveFieldChanged(field)); + } + }); + await dataController.reloadTypeOption(); + add(FieldEditorEvent.didReceiveFieldChanged(dataController.field)); + }, + updateName: (name) { + if (state.name != name) { + dataController.fieldName = name; + emit(state.copyWith(name: name)); + } + }, + didReceiveFieldChanged: (FieldPB field) { + emit( + state.copyWith( + field: Some(field), + name: field.name, + canDelete: field.isPrimary, + ), + ); + }, + deleteField: () { + state.field.fold( + () => null, + (field) { + final fieldService = FieldBackendService( + viewId: loader.viewId, + fieldId: field.id, + ); + fieldService.deleteField(); + }, + ); + }, + switchToField: (FieldType fieldType) async { + await dataController.switchToField(fieldType); + }, + ); + }, + ); + } +} + +@freezed +class FieldEditorEvent with _$FieldEditorEvent { + const factory FieldEditorEvent.initial() = _InitialField; + const factory FieldEditorEvent.updateName(String name) = _UpdateName; + const factory FieldEditorEvent.deleteField() = _DeleteField; + const factory FieldEditorEvent.switchToField(FieldType fieldType) = + _SwitchToField; + const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) = + _DidReceiveFieldChanged; +} + +@freezed +class FieldEditorState with _$FieldEditorState { + const factory FieldEditorState({ + required String viewId, + required String errorText, + required String name, + required Option field, + required bool canDelete, + required bool isGroupField, + }) = _FieldEditorState; + + factory FieldEditorState.initial( + String viewId, + String fieldName, + bool isGroupField, + ) => + FieldEditorState( + viewId: viewId, + errorText: '', + field: none(), + canDelete: false, + name: fieldName, + isGroupField: isGroupField, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart new file mode 100644 index 0000000000..09ecaac807 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart @@ -0,0 +1,91 @@ +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; + +typedef UpdateFieldNotifiedValue = FieldPB; + +class SingleFieldListener { + final String fieldId; + void Function(UpdateFieldNotifiedValue)? _updateFieldNotifier; + DatabaseNotificationListener? _listener; + + SingleFieldListener({required this.fieldId}); + + void start({ + required void Function(UpdateFieldNotifiedValue) onFieldChanged, + }) { + _updateFieldNotifier = onFieldChanged; + _listener = DatabaseNotificationListener( + objectId: fieldId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateField: + result.fold( + (payload) => _updateFieldNotifier?.call(FieldPB.fromBuffer(payload)), + (error) => Log.error(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _updateFieldNotifier = null; + } +} + +typedef UpdateFieldsNotifiedValue + = Either; + +class FieldsListener { + final String viewId; + PublishNotifier? updateFieldsNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + FieldsListener({required this.viewId}); + + void start({ + required void Function(UpdateFieldsNotifiedValue) onFieldsChanged, + }) { + updateFieldsNotifier?.addPublishListener(onFieldsChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler(DatabaseNotification ty, Either result) { + switch (ty) { + case DatabaseNotification.DidUpdateFields: + result.fold( + (payload) => updateFieldsNotifier?.value = + left(DatabaseFieldChangesetPB.fromBuffer(payload)), + (error) => updateFieldsNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + updateFieldsNotifier?.dispose(); + updateFieldsNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart new file mode 100644 index 0000000000..05b1b30e71 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart @@ -0,0 +1,118 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'field_service.freezed.dart'; + +/// FieldService consists of lots of event functions. We define the events in the backend(Rust), +/// you can find the corresponding event implementation in event_map.rs of the corresponding crate. +/// +/// You could check out the rust-lib/flowy-database/event_map.rs for more information. +class FieldBackendService { + final String viewId; + final String fieldId; + + FieldBackendService({required this.viewId, required this.fieldId}); + + Future> moveField(int fromIndex, int toIndex) { + final payload = MoveFieldPayloadPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..fromIndex = fromIndex + ..toIndex = toIndex; + + return DatabaseEventMoveField(payload).send(); + } + + Future> updateField({ + String? name, + bool? frozen, + bool? visibility, + double? width, + }) { + final payload = FieldChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId; + + if (name != null) { + payload.name = name; + } + + if (frozen != null) { + payload.frozen = frozen; + } + + if (visibility != null) { + payload.visibility = visibility; + } + + if (width != null) { + payload.width = width.toInt(); + } + + return DatabaseEventUpdateField(payload).send(); + } + + static Future> updateFieldTypeOption({ + required String viewId, + required String fieldId, + required List typeOptionData, + }) { + final payload = TypeOptionChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..typeOptionData = typeOptionData; + + return DatabaseEventUpdateFieldTypeOption(payload).send(); + } + + Future> deleteField() { + final payload = DeleteFieldPayloadPB.create() + ..viewId = viewId + ..fieldId = fieldId; + + return DatabaseEventDeleteField(payload).send(); + } + + Future> duplicateField() { + final payload = DuplicateFieldPayloadPB.create() + ..viewId = viewId + ..fieldId = fieldId; + + return DatabaseEventDuplicateField(payload).send(); + } + + Future> getFieldTypeOptionData({ + required FieldType fieldType, + }) { + final payload = TypeOptionPathPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..fieldType = fieldType; + return DatabaseEventGetTypeOption(payload).send().then((result) { + return result.fold( + (data) => left(data), + (err) => right(err), + ); + }); + } + + /// Returns the primary field of the view. + static Future> getPrimaryField({ + required String viewId, + }) { + final payload = DatabaseViewIdPB.create()..value = viewId; + return DatabaseEventGetPrimaryField(payload).send(); + } +} + +@freezed +class FieldContext with _$FieldContext { + const factory FieldContext({ + required String viewId, + required FieldPB field, + }) = _FieldCellContext; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart new file mode 100644 index 0000000000..106d5563ef --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart @@ -0,0 +1,66 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import 'type_option/type_option_data_controller.dart'; +part 'field_type_option_edit_bloc.freezed.dart'; + +class FieldTypeOptionEditBloc + extends Bloc { + final TypeOptionController _dataController; + void Function()? _fieldListenFn; + + FieldTypeOptionEditBloc(TypeOptionController dataController) + : _dataController = dataController, + super(FieldTypeOptionEditState.initial(dataController)) { + on( + (event, emit) async { + event.when( + initial: () { + _fieldListenFn = dataController.addFieldListener((field) { + add(FieldTypeOptionEditEvent.didReceiveFieldUpdated(field)); + }); + }, + didReceiveFieldUpdated: (field) { + emit(state.copyWith(field: field)); + }, + switchToField: (FieldType fieldType) async { + await _dataController.switchToField(fieldType); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_fieldListenFn != null) { + _dataController.removeFieldListener(_fieldListenFn!); + } + return super.close(); + } +} + +@freezed +class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent { + const factory FieldTypeOptionEditEvent.initial() = _Initial; + const factory FieldTypeOptionEditEvent.switchToField(FieldType fieldType) = + _SwitchToField; + const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(FieldPB field) = + _DidReceiveFieldUpdated; +} + +@freezed +class FieldTypeOptionEditState with _$FieldTypeOptionEditState { + const factory FieldTypeOptionEditState({ + required FieldPB field, + }) = _FieldTypeOptionEditState; + + factory FieldTypeOptionEditState.initial( + TypeOptionController typeOptionController, + ) => + FieldTypeOptionEditState( + field: typeOptionController.field, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart new file mode 100644 index 0000000000..637557c75e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart @@ -0,0 +1,78 @@ +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 'package:protobuf/protobuf.dart'; + +import 'type_option_context.dart'; +part 'date_bloc.freezed.dart'; + +class DateTypeOptionBloc + extends Bloc { + DateTypeOptionBloc({required DateTypeOptionContext typeOptionContext}) + : super(DateTypeOptionState.initial(typeOptionContext.typeOption)) { + on( + (event, emit) async { + event.map( + didSelectDateFormat: (_DidSelectDateFormat value) { + emit( + state.copyWith( + typeOption: _updateTypeOption(dateFormat: value.format), + ), + ); + }, + didSelectTimeFormat: (_DidSelectTimeFormat value) { + emit( + state.copyWith( + typeOption: _updateTypeOption(timeFormat: value.format), + ), + ); + }, + includeTime: (_IncludeTime value) { + emit( + state.copyWith( + typeOption: _updateTypeOption(includeTime: value.includeTime), + ), + ); + }, + ); + }, + ); + } + + DateTypeOptionPB _updateTypeOption({ + DateFormatPB? dateFormat, + TimeFormatPB? timeFormat, + bool? includeTime, + }) { + state.typeOption.freeze(); + return state.typeOption.rebuild((typeOption) { + if (dateFormat != null) { + typeOption.dateFormat = dateFormat; + } + + if (timeFormat != null) { + typeOption.timeFormat = timeFormat; + } + }); + } +} + +@freezed +class DateTypeOptionEvent with _$DateTypeOptionEvent { + const factory DateTypeOptionEvent.didSelectDateFormat(DateFormatPB format) = + _DidSelectDateFormat; + const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) = + _DidSelectTimeFormat; + const factory DateTypeOptionEvent.includeTime(bool includeTime) = + _IncludeTime; +} + +@freezed +class DateTypeOptionState with _$DateTypeOptionState { + const factory DateTypeOptionState({ + required DateTypeOptionPB typeOption, + }) = _DateTypeOptionState; + + factory DateTypeOptionState.initial(DateTypeOptionPB typeOption) => + DateTypeOptionState(typeOption: typeOption); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/edit_select_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart similarity index 77% rename from frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/edit_select_option_bloc.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart index 59abf7d136..296d3c7b58 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/edit_select_option_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart @@ -1,8 +1,8 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; - +import 'package:dartz/dartz.dart'; part 'edit_select_option_bloc.freezed.dart'; class EditSelectOptionBloc @@ -11,15 +11,15 @@ class EditSelectOptionBloc : super(EditSelectOptionState.initial(option)) { on( (event, emit) async { - event.when( - updateName: (name) { - emit(state.copyWith(option: _updateName(name))); + event.map( + updateName: (_UpdateName value) { + emit(state.copyWith(option: _updateName(value.name))); }, - updateColor: (color) { - emit(state.copyWith(option: _updateColor(color))); + updateColor: (_UpdateColor value) { + emit(state.copyWith(option: _updateColor(value.color))); }, - delete: () { - emit(state.copyWith(deleted: true)); + delete: (_Delete value) { + emit(state.copyWith(deleted: const Some(true))); }, ); }, @@ -53,12 +53,12 @@ class EditSelectOptionEvent with _$EditSelectOptionEvent { class EditSelectOptionState with _$EditSelectOptionState { const factory EditSelectOptionState({ required SelectOptionPB option, - required bool deleted, + required Option deleted, }) = _EditSelectOptionState; factory EditSelectOptionState.initial(SelectOptionPB option) => EditSelectOptionState( option: option, - deleted: false, + deleted: none(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/multi_select_type_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/multi_select_type_option.dart new file mode 100644 index 0000000000..b22696c559 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/multi_select_type_option.dart @@ -0,0 +1,85 @@ +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'dart:async'; +import 'select_option_type_option_bloc.dart'; +import 'type_option_context.dart'; +import 'type_option_service.dart'; +import 'package:protobuf/protobuf.dart'; + +class MultiSelectAction with ISelectOptionAction { + final String viewId; + final String fieldId; + final TypeOptionBackendService service; + final MultiSelectTypeOptionContext typeOptionContext; + + MultiSelectAction({ + required this.viewId, + required this.fieldId, + required this.typeOptionContext, + }) : service = TypeOptionBackendService( + viewId: viewId, + fieldId: fieldId, + ); + + MultiSelectTypeOptionPB get typeOption => typeOptionContext.typeOption; + + set typeOption(MultiSelectTypeOptionPB newTypeOption) { + typeOptionContext.typeOption = newTypeOption; + } + + @override + List Function(SelectOptionPB) get deleteOption { + return (SelectOptionPB option) { + typeOption.freeze(); + typeOption = typeOption.rebuild((typeOption) { + final index = + typeOption.options.indexWhere((element) => element.id == option.id); + if (index != -1) { + typeOption.options.removeAt(index); + } + }); + return typeOption.options; + }; + } + + @override + Future> Function(String) get insertOption { + return (String optionName) { + return service.newOption(name: optionName).then((result) { + return result.fold( + (option) { + typeOption.freeze(); + typeOption = typeOption.rebuild((typeOption) { + final exists = typeOption.options + .any((element) => element.name == option.name); + if (!exists) { + typeOption.options.insert(0, option); + } + }); + + return typeOption.options; + }, + (err) { + Log.error(err); + return typeOption.options; + }, + ); + }); + }; + } + + @override + List Function(SelectOptionPB) get updateOption { + return (SelectOptionPB option) { + typeOption.freeze(); + typeOption = typeOption.rebuild((typeOption) { + final index = + typeOption.options.indexWhere((element) => element.id == option.id); + if (index != -1) { + typeOption.options[index] = option; + } + }); + return typeOption.options; + }; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_bloc.dart new file mode 100644 index 0000000000..400dfe3278 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_bloc.dart @@ -0,0 +1,48 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; +import 'type_option_context.dart'; + +part 'number_bloc.freezed.dart'; + +class NumberTypeOptionBloc + extends Bloc { + NumberTypeOptionBloc({required NumberTypeOptionContext typeOptionContext}) + : super(NumberTypeOptionState.initial(typeOptionContext.typeOption)) { + on( + (event, emit) async { + event.map( + didSelectFormat: (_DidSelectFormat value) { + emit(state.copyWith(typeOption: _updateNumberFormat(value.format))); + }, + ); + }, + ); + } + + NumberTypeOptionPB _updateNumberFormat(NumberFormatPB format) { + state.typeOption.freeze(); + return state.typeOption.rebuild((typeOption) { + typeOption.format = format; + }); + } +} + +@freezed +class NumberTypeOptionEvent with _$NumberTypeOptionEvent { + const factory NumberTypeOptionEvent.didSelectFormat(NumberFormatPB format) = + _DidSelectFormat; +} + +@freezed +class NumberTypeOptionState with _$NumberTypeOptionState { + const factory NumberTypeOptionState({ + required NumberTypeOptionPB typeOption, + }) = _NumberTypeOptionState; + + factory NumberTypeOptionState.initial(NumberTypeOptionPB typeOption) => + NumberTypeOptionState( + typeOption: typeOption, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_format_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_format_bloc.dart new file mode 100644 index 0000000000..b6932ce0ac --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_format_bloc.dart @@ -0,0 +1,144 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'number_format_bloc.freezed.dart'; + +class NumberFormatBloc extends Bloc { + NumberFormatBloc() : super(NumberFormatState.initial()) { + on( + (event, emit) async { + event.map( + setFilter: (_SetFilter value) { + final List formats = + List.from(NumberFormatPB.values); + if (value.filter.isNotEmpty) { + formats.retainWhere( + (element) => element + .title() + .toLowerCase() + .contains(value.filter.toLowerCase()), + ); + } + emit(state.copyWith(formats: formats, filter: value.filter)); + }, + ); + }, + ); + } +} + +@freezed +class NumberFormatEvent with _$NumberFormatEvent { + const factory NumberFormatEvent.setFilter(String filter) = _SetFilter; +} + +@freezed +class NumberFormatState with _$NumberFormatState { + const factory NumberFormatState({ + required List formats, + required String filter, + }) = _NumberFormatState; + + factory NumberFormatState.initial() { + return const NumberFormatState( + formats: NumberFormatPB.values, + filter: "", + ); + } +} + +extension NumberFormatExtension on NumberFormatPB { + String title() { + switch (this) { + case NumberFormatPB.ArgentinePeso: + return "Argentine peso"; + case NumberFormatPB.Baht: + return "Baht"; + case NumberFormatPB.CanadianDollar: + return "Canadian dollar"; + case NumberFormatPB.ChileanPeso: + return "Chilean peso"; + case NumberFormatPB.ColombianPeso: + return "Colombian peso"; + case NumberFormatPB.DanishKrone: + return "Danish krone"; + case NumberFormatPB.Dirham: + return "Dirham"; + case NumberFormatPB.EUR: + return "Euro"; + case NumberFormatPB.Forint: + return "Forint"; + case NumberFormatPB.Franc: + return "Franc"; + case NumberFormatPB.HongKongDollar: + return "Hone Kong dollar"; + case NumberFormatPB.Koruna: + return "Koruna"; + case NumberFormatPB.Krona: + return "Krona"; + case NumberFormatPB.Leu: + return "Leu"; + case NumberFormatPB.Lira: + return "Lira"; + case NumberFormatPB.MexicanPeso: + return "Mexican peso"; + case NumberFormatPB.NewTaiwanDollar: + return "New Taiwan dollar"; + case NumberFormatPB.NewZealandDollar: + return "New Zealand dollar"; + case NumberFormatPB.NorwegianKrone: + return "Norwegian krone"; + case NumberFormatPB.Num: + return "Number"; + case NumberFormatPB.Percent: + return "Percent"; + case NumberFormatPB.PhilippinePeso: + return "Philippine peso"; + case NumberFormatPB.Pound: + return "Pound"; + case NumberFormatPB.Rand: + return "Rand"; + case NumberFormatPB.Real: + return "Real"; + case NumberFormatPB.Ringgit: + return "Ringgit"; + case NumberFormatPB.Riyal: + return "Riyal"; + case NumberFormatPB.Ruble: + return "Ruble"; + case NumberFormatPB.Rupee: + return "Rupee"; + case NumberFormatPB.Rupiah: + return "Rupiah"; + case NumberFormatPB.Shekel: + return "Skekel"; + case NumberFormatPB.USD: + return "US dollar"; + case NumberFormatPB.UruguayanPeso: + return "Uruguayan peso"; + case NumberFormatPB.Won: + return "Won"; + case NumberFormatPB.Yen: + return "Yen"; + case NumberFormatPB.Yuan: + return "Yuan"; + default: + throw UnimplementedError; + } + } + + // String iconName() { + // switch (this) { + // case NumberFormatPB.CNY: + // return "grid/field/yen"; + // case NumberFormatPB.EUR: + // return "grid/field/euro"; + // case NumberFormatPB.Number: + // return "grid/field/numbers"; + // case NumberFormatPB.USD: + // return "grid/field/us_dollar"; + // default: + // throw UnimplementedError; + // } + // } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart new file mode 100644 index 0000000000..23f23c8e38 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart @@ -0,0 +1,83 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'package:dartz/dartz.dart'; +part 'select_option_type_option_bloc.freezed.dart'; + +abstract mixin class ISelectOptionAction { + Future> Function(String) get insertOption; + + List Function(SelectOptionPB) get deleteOption; + + List Function(SelectOptionPB) get updateOption; +} + +class SelectOptionTypeOptionBloc + extends Bloc { + final ISelectOptionAction typeOptionAction; + + SelectOptionTypeOptionBloc({ + required List options, + required this.typeOptionAction, + }) : super(SelectOptionTypeOptionState.initial(options)) { + on( + (event, emit) async { + await event.when( + createOption: (optionName) async { + final List options = + await typeOptionAction.insertOption(optionName); + emit(state.copyWith(options: options)); + }, + addingOption: () { + emit(state.copyWith(isEditingOption: true, newOptionName: none())); + }, + endAddingOption: () { + emit(state.copyWith(isEditingOption: false, newOptionName: none())); + }, + updateOption: (option) { + final List options = + typeOptionAction.updateOption(option); + emit(state.copyWith(options: options)); + }, + deleteOption: (option) { + final List options = + typeOptionAction.deleteOption(option); + emit(state.copyWith(options: options)); + }, + ); + }, + ); + } +} + +@freezed +class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent { + const factory SelectOptionTypeOptionEvent.createOption(String optionName) = + _CreateOption; + const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption; + const factory SelectOptionTypeOptionEvent.endAddingOption() = + _EndAddingOption; + const factory SelectOptionTypeOptionEvent.updateOption( + SelectOptionPB option, + ) = _UpdateOption; + const factory SelectOptionTypeOptionEvent.deleteOption( + SelectOptionPB option, + ) = _DeleteOption; +} + +@freezed +class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState { + const factory SelectOptionTypeOptionState({ + required List options, + required bool isEditingOption, + required Option newOptionName, + }) = _SelectOptionTypeOptionState; + + factory SelectOptionTypeOptionState.initial(List options) => + SelectOptionTypeOptionState( + options: options, + isEditingOption: false, + newOptionName: none(), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/single_select_type_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/single_select_type_option.dart new file mode 100644 index 0000000000..9114070f9d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/single_select_type_option.dart @@ -0,0 +1,82 @@ +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'dart:async'; +import 'package:protobuf/protobuf.dart'; +import 'select_option_type_option_bloc.dart'; +import 'type_option_context.dart'; +import 'type_option_service.dart'; + +class SingleSelectAction with ISelectOptionAction { + final String viewId; + final String fieldId; + final SingleSelectTypeOptionContext typeOptionContext; + final TypeOptionBackendService service; + + SingleSelectAction({ + required this.viewId, + required this.fieldId, + required this.typeOptionContext, + }) : service = TypeOptionBackendService(viewId: viewId, fieldId: fieldId); + + SingleSelectTypeOptionPB get typeOption => typeOptionContext.typeOption; + + set typeOption(SingleSelectTypeOptionPB newTypeOption) { + typeOptionContext.typeOption = newTypeOption; + } + + @override + List Function(SelectOptionPB) get deleteOption { + return (SelectOptionPB option) { + typeOption.freeze(); + typeOption = typeOption.rebuild((typeOption) { + final index = + typeOption.options.indexWhere((element) => element.id == option.id); + if (index != -1) { + typeOption.options.removeAt(index); + } + }); + return typeOption.options; + }; + } + + @override + Future> Function(String) get insertOption { + return (String optionName) { + return service.newOption(name: optionName).then((result) { + return result.fold( + (option) { + typeOption.freeze(); + typeOption = typeOption.rebuild((typeOption) { + final exists = typeOption.options + .any((element) => element.name == option.name); + if (!exists) { + typeOption.options.insert(0, option); + } + }); + + return typeOption.options; + }, + (err) { + Log.error(err); + return typeOption.options; + }, + ); + }); + }; + } + + @override + List Function(SelectOptionPB) get updateOption { + return (SelectOptionPB option) { + typeOption.freeze(); + typeOption = typeOption.rebuild((typeOption) { + final index = + typeOption.options.indexWhere((element) => element.id == option.id); + if (index != -1) { + typeOption.options[index] = option; + } + }); + return typeOption.options; + }; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart new file mode 100644 index 0000000000..a79de8ee61 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart @@ -0,0 +1,178 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:protobuf/protobuf.dart'; +import 'type_option_data_controller.dart'; + +abstract class TypeOptionParser { + T fromBuffer(List buffer); +} + +// Number +typedef NumberTypeOptionContext = TypeOptionContext; + +class NumberTypeOptionWidgetDataParser + extends TypeOptionParser { + @override + NumberTypeOptionPB fromBuffer(List buffer) { + return NumberTypeOptionPB.fromBuffer(buffer); + } +} + +// RichText +typedef RichTextTypeOptionContext = TypeOptionContext; + +class RichTextTypeOptionWidgetDataParser + extends TypeOptionParser { + @override + RichTextTypeOptionPB fromBuffer(List buffer) { + return RichTextTypeOptionPB.fromBuffer(buffer); + } +} + +// Checkbox +typedef CheckboxTypeOptionContext = TypeOptionContext; + +class CheckboxTypeOptionWidgetDataParser + extends TypeOptionParser { + @override + CheckboxTypeOptionPB fromBuffer(List buffer) { + return CheckboxTypeOptionPB.fromBuffer(buffer); + } +} + +// URL +typedef URLTypeOptionContext = TypeOptionContext; + +class URLTypeOptionWidgetDataParser extends TypeOptionParser { + @override + URLTypeOptionPB fromBuffer(List buffer) { + return URLTypeOptionPB.fromBuffer(buffer); + } +} + +// Date +typedef DateTypeOptionContext = TypeOptionContext; + +class DateTypeOptionDataParser extends TypeOptionParser { + @override + DateTypeOptionPB fromBuffer(List buffer) { + return DateTypeOptionPB.fromBuffer(buffer); + } +} + +// SingleSelect +typedef SingleSelectTypeOptionContext + = TypeOptionContext; + +class SingleSelectTypeOptionWidgetDataParser + extends TypeOptionParser { + @override + SingleSelectTypeOptionPB fromBuffer(List buffer) { + return SingleSelectTypeOptionPB.fromBuffer(buffer); + } +} + +// Multi-select +typedef MultiSelectTypeOptionContext + = TypeOptionContext; + +class MultiSelectTypeOptionWidgetDataParser + extends TypeOptionParser { + @override + MultiSelectTypeOptionPB fromBuffer(List buffer) { + return MultiSelectTypeOptionPB.fromBuffer(buffer); + } +} + +// Multi-select +typedef ChecklistTypeOptionContext = TypeOptionContext; + +class ChecklistTypeOptionWidgetDataParser + extends TypeOptionParser { + @override + ChecklistTypeOptionPB fromBuffer(List buffer) { + return ChecklistTypeOptionPB.fromBuffer(buffer); + } +} + +class TypeOptionContext { + T? _typeOptionObject; + final TypeOptionParser dataParser; + final TypeOptionController _dataController; + + TypeOptionContext({ + required this.dataParser, + required TypeOptionController dataController, + }) : _dataController = dataController; + + String get viewId => _dataController.loader.viewId; + + String get fieldId => _dataController.field.id; + + Future loadTypeOptionData({ + void Function(T)? onCompleted, + required void Function(FlowyError) onError, + }) async { + await _dataController.reloadTypeOption().then((result) { + result.fold((l) => null, (err) => onError(err)); + }); + + onCompleted?.call(typeOption); + return typeOption; + } + + T get typeOption { + if (_typeOptionObject != null) { + return _typeOptionObject!; + } + + final T object = _dataController.getTypeOption(dataParser); + _typeOptionObject = object; + return object; + } + + set typeOption(T typeOption) { + _dataController.typeOptionData = typeOption.writeToBuffer(); + _typeOptionObject = typeOption; + } +} + +abstract class TypeOptionFieldDelegate { + void onFieldChanged(void Function(String) callback); + void dispose(); +} + +abstract class ITypeOptionLoader { + String get viewId; + String get fieldName; + + Future> initialize(); +} + +/// Uses when editing a existing field +class FieldTypeOptionLoader { + final String viewId; + final FieldPB field; + + FieldTypeOptionLoader({ + required this.viewId, + required this.field, + }); + + Future> load() { + final payload = TypeOptionPathPB.create() + ..viewId = viewId + ..fieldId = field.id + ..fieldType = field.fieldType; + + return DatabaseEventGetTypeOption(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart new file mode 100644 index 0000000000..e793b72f8e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:protobuf/protobuf.dart' hide FieldInfo; +import 'package:appflowy_backend/log.dart'; + +import '../field_service.dart'; +import 'type_option_context.dart'; + +class TypeOptionController { + late TypeOptionPB _typeOption; + final FieldTypeOptionLoader loader; + final PublishNotifier _fieldNotifier = PublishNotifier(); + + /// Returns a [TypeOptionController] used to modify the specified + /// [FieldPB]'s data + /// + /// Should call [reloadTypeOption] if the passed-in [FieldInfo] + /// is null + /// + TypeOptionController({ + required this.loader, + required FieldPB field, + }) { + _typeOption = TypeOptionPB.create() + ..viewId = loader.viewId + ..field_2 = field; + } + + Future> reloadTypeOption() async { + final result = await loader.load(); + return result.fold( + (data) { + data.freeze(); + _typeOption = data; + _fieldNotifier.value = data.field_2; + return left(data); + }, + (err) { + Log.error(err); + return right(err); + }, + ); + } + + FieldPB get field { + return _typeOption.field_2; + } + + T getTypeOption(TypeOptionParser parser) { + return parser.fromBuffer(_typeOption.typeOptionData); + } + + set fieldName(String name) { + _typeOption = _typeOption.rebuild((rebuildData) { + rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) { + rebuildField.name = name; + }); + }); + + _fieldNotifier.value = _typeOption.field_2; + + FieldBackendService(viewId: loader.viewId, fieldId: field.id) + .updateField(name: name); + } + + set typeOptionData(List typeOptionData) { + _typeOption = _typeOption.rebuild((rebuildData) { + if (typeOptionData.isNotEmpty) { + rebuildData.typeOptionData = typeOptionData; + } + }); + + FieldBackendService.updateFieldTypeOption( + viewId: loader.viewId, + fieldId: field.id, + typeOptionData: typeOptionData, + ); + } + + Future switchToField(FieldType newFieldType) async { + final payload = UpdateFieldTypePayloadPB.create() + ..viewId = loader.viewId + ..fieldId = field.id + ..fieldType = newFieldType; + + final result = await DatabaseEventUpdateFieldType(payload).send(); + await result.fold( + (_) { + // Should load the type-option data after switching to a new field. + // After loading the type-option data, the editor widget that uses + // the type-option data will be rebuild. + reloadTypeOption(); + }, + (err) => Future(() => Log.error(err)), + ); + } + + void Function() addFieldListener(void Function(FieldPB) callback) { + listener() { + callback(field); + } + + _fieldNotifier.addListener(listener); + return listener; + } + + void removeFieldListener(void Function() listener) { + _fieldNotifier.removeListener(listener); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart new file mode 100644 index 0000000000..7c1fd9a71c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart @@ -0,0 +1,36 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; + +class TypeOptionBackendService { + final String viewId; + final String fieldId; + + TypeOptionBackendService({ + required this.viewId, + required this.fieldId, + }); + + Future> newOption({ + required String name, + }) { + final payload = CreateSelectOptionPayloadPB.create() + ..optionName = name + ..viewId = viewId + ..fieldId = fieldId; + + return DatabaseEventCreateSelectOption(payload).send(); + } + + static Future> createFieldTypeOption({ + required String viewId, + FieldType fieldType = FieldType.RichText, + }) { + final payload = CreateFieldPayloadPB.create() + ..viewId = viewId + ..fieldType = FieldType.RichText; + + return DatabaseEventCreateTypeOption(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_listener.dart new file mode 100644 index 0000000000..0170f97772 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_listener.dart @@ -0,0 +1,128 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; + +typedef UpdateFilterNotifiedValue + = Either; + +class FiltersListener { + final String viewId; + + PublishNotifier? _filterNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + FiltersListener({required this.viewId}); + + void start({ + required void Function(UpdateFilterNotifiedValue) onFilterChanged, + }) { + _filterNotifier?.addPublishListener(onFilterChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateFilter: + result.fold( + (payload) => _filterNotifier?.value = + left(FilterChangesetNotificationPB.fromBuffer(payload)), + (error) => _filterNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _filterNotifier?.dispose(); + _filterNotifier = null; + } +} + +class FilterListener { + final String viewId; + final String filterId; + + PublishNotifier? _onDeleteNotifier = PublishNotifier(); + PublishNotifier? _onUpdateNotifier = PublishNotifier(); + + DatabaseNotificationListener? _listener; + FilterListener({required this.viewId, required this.filterId}); + + void start({ + void Function()? onDeleted, + void Function(FilterPB)? onUpdated, + }) { + _onDeleteNotifier?.addPublishListener((_) { + onDeleted?.call(); + }); + + _onUpdateNotifier?.addPublishListener((filter) { + onUpdated?.call(filter); + }); + + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void handleChangeset(FilterChangesetNotificationPB changeset) { + // check the delete filter + final deletedIndex = changeset.deleteFilters.indexWhere( + (element) => element.id == filterId, + ); + if (deletedIndex != -1) { + _onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex]; + } + + // check the updated filter + final updatedIndex = changeset.updateFilters.indexWhere( + (element) => element.filter.id == filterId, + ); + if (updatedIndex != -1) { + _onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter; + } + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateFilter: + result.fold( + (payload) => handleChangeset( + FilterChangesetNotificationPB.fromBuffer(payload), + ), + (error) {}, + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _onDeleteNotifier?.dispose(); + _onDeleteNotifier = null; + + _onUpdateNotifier?.dispose(); + _onUpdateNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart new file mode 100644 index 0000000000..c600b1ec2e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart @@ -0,0 +1,230 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:dartz/dartz.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-database2/checkbox_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbserver.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.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; +import 'package:fixnum/fixnum.dart' as $fixnum; + +class FilterBackendService { + final String viewId; + const FilterBackendService({required this.viewId}); + + Future, FlowyError>> getAllFilters() { + final payload = DatabaseViewIdPB()..value = viewId; + + return DatabaseEventGetAllFilters(payload).send().then((result) { + return result.fold( + (repeated) => left(repeated.items), + (r) => right(r), + ); + }); + } + + Future> insertTextFilter({ + required String fieldId, + String? filterId, + required TextFilterConditionPB condition, + required String content, + }) { + final filter = TextFilterPB() + ..condition = condition + ..content = content; + + return insertFilter( + fieldId: fieldId, + filterId: filterId, + fieldType: FieldType.RichText, + data: filter.writeToBuffer(), + ); + } + + Future> insertCheckboxFilter({ + required String fieldId, + String? filterId, + required CheckboxFilterConditionPB condition, + }) { + final filter = CheckboxFilterPB()..condition = condition; + + return insertFilter( + fieldId: fieldId, + filterId: filterId, + fieldType: FieldType.Checkbox, + data: filter.writeToBuffer(), + ); + } + + Future> insertNumberFilter({ + required String fieldId, + String? filterId, + required NumberFilterConditionPB condition, + String content = "", + }) { + final filter = NumberFilterPB() + ..condition = condition + ..content = content; + + return insertFilter( + fieldId: fieldId, + filterId: filterId, + fieldType: FieldType.Number, + data: filter.writeToBuffer(), + ); + } + + Future> insertDateFilter({ + required String fieldId, + String? filterId, + required DateFilterConditionPB condition, + required FieldType fieldType, + int? start, + int? end, + int? timestamp, + }) { + assert( + [ + FieldType.DateTime, + FieldType.LastEditedTime, + FieldType.CreatedTime, + ].contains(fieldType), + ); + + final filter = DateFilterPB(); + if (timestamp != null) { + filter.timestamp = $fixnum.Int64(timestamp); + } else { + if (start != null && end != null) { + filter.start = $fixnum.Int64(start); + filter.end = $fixnum.Int64(end); + } else { + throw Exception( + "Start and end should not be null if the timestamp is null", + ); + } + } + + return insertFilter( + fieldId: fieldId, + filterId: filterId, + fieldType: fieldType, + data: filter.writeToBuffer(), + ); + } + + Future> insertURLFilter({ + required String fieldId, + String? filterId, + required TextFilterConditionPB condition, + String content = "", + }) { + final filter = TextFilterPB() + ..condition = condition + ..content = content; + + return insertFilter( + fieldId: fieldId, + filterId: filterId, + fieldType: FieldType.URL, + data: filter.writeToBuffer(), + ); + } + + Future> insertSelectOptionFilter({ + required String fieldId, + required FieldType fieldType, + required SelectOptionConditionPB condition, + String? filterId, + List optionIds = const [], + }) { + final filter = SelectOptionFilterPB() + ..condition = condition + ..optionIds.addAll(optionIds); + + return insertFilter( + fieldId: fieldId, + filterId: filterId, + fieldType: fieldType, + data: filter.writeToBuffer(), + ); + } + + Future> insertChecklistFilter({ + required String fieldId, + required ChecklistFilterConditionPB condition, + String? filterId, + List optionIds = const [], + }) { + final filter = ChecklistFilterPB()..condition = condition; + + return insertFilter( + fieldId: fieldId, + filterId: filterId, + fieldType: FieldType.Checklist, + data: filter.writeToBuffer(), + ); + } + + Future> insertFilter({ + required String fieldId, + String? filterId, + required FieldType fieldType, + required List data, + }) { + final insertFilterPayload = UpdateFilterPayloadPB.create() + ..fieldId = fieldId + ..fieldType = fieldType + ..viewId = viewId + ..data = data; + + if (filterId != null) { + insertFilterPayload.filterId = filterId; + } + + final payload = DatabaseSettingChangesetPB.create() + ..viewId = viewId + ..updateFilter = insertFilterPayload; + return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { + return result.fold( + (l) => left(l), + (err) { + Log.error(err); + return right(err); + }, + ); + }); + } + + Future> deleteFilter({ + required String fieldId, + required String filterId, + required FieldType fieldType, + }) { + final deleteFilterPayload = DeleteFilterPayloadPB.create() + ..fieldId = fieldId + ..filterId = filterId + ..viewId = viewId + ..fieldType = fieldType; + + final payload = DatabaseSettingChangesetPB.create() + ..viewId = viewId + ..deleteFilter = deleteFilterPayload; + + return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { + return result.fold( + (l) => left(l), + (err) { + Log.error(err); + return right(err); + }, + ); + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart new file mode 100644 index 0000000000..3745170937 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart @@ -0,0 +1,66 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; + +typedef GroupUpdateValue = Either; +typedef GroupByNewFieldValue = Either, FlowyError>; + +class DatabaseGroupListener { + final String viewId; + PublishNotifier? _numOfGroupsNotifier = PublishNotifier(); + PublishNotifier? _groupByFieldNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + DatabaseGroupListener(this.viewId); + + void start({ + required void Function(GroupUpdateValue) onNumOfGroupsChanged, + required void Function(GroupByNewFieldValue) onGroupByNewField, + }) { + _numOfGroupsNotifier?.addPublishListener(onNumOfGroupsChanged); + _groupByFieldNotifier?.addPublishListener(onGroupByNewField); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateNumOfGroups: + result.fold( + (payload) => _numOfGroupsNotifier?.value = + left(GroupChangesPB.fromBuffer(payload)), + (error) => _numOfGroupsNotifier?.value = right(error), + ); + break; + case DatabaseNotification.DidGroupByField: + result.fold( + (payload) => _groupByFieldNotifier?.value = + left(GroupChangesPB.fromBuffer(payload).initialGroups), + (error) => _groupByFieldNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _numOfGroupsNotifier?.dispose(); + _numOfGroupsNotifier = null; + + _groupByFieldNotifier?.dispose(); + _groupByFieldNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart new file mode 100644 index 0000000000..9ab4bad320 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart @@ -0,0 +1,34 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; + +class GroupBackendService { + final String viewId; + GroupBackendService(this.viewId); + + Future> groupByField({ + required String fieldId, + }) { + final payload = GroupByFieldPayloadPB.create() + ..viewId = viewId + ..fieldId = fieldId; + + return DatabaseEventSetGroupByField(payload).send(); + } + + Future> updateGroup({ + required String groupId, + String? name, + bool? visible, + }) { + final payload = UpdateGroupPB.create()..groupId = groupId; + if (name != null) { + payload.name = name; + } + if (visible != null) { + payload.visible = visible; + } + return DatabaseEventUpdateGroup(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart new file mode 100644 index 0000000000..57e2c112a0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; + +typedef NewLayoutFieldValue = Either; + +class DatabaseCalendarLayoutListener { + final String viewId; + PublishNotifier? _newLayoutFieldNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + DatabaseCalendarLayoutListener(this.viewId); + + void start({ + required void Function(NewLayoutFieldValue) onCalendarLayoutChanged, + }) { + _newLayoutFieldNotifier?.addPublishListener(onCalendarLayoutChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidSetNewLayoutField: + result.fold( + (payload) => _newLayoutFieldNotifier?.value = + left(DatabaseLayoutSettingPB.fromBuffer(payload)), + (error) => _newLayoutFieldNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _newLayoutFieldNotifier?.dispose(); + _newLayoutFieldNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart similarity index 82% rename from frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart index 0f884a1e9a..1098a39f59 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart @@ -1,23 +1,26 @@ -import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'layout_service.dart'; part 'layout_bloc.freezed.dart'; class DatabaseLayoutBloc extends Bloc { + final DatabaseLayoutBackendService layoutService; + DatabaseLayoutBloc({ required String viewId, required DatabaseLayoutPB databaseLayout, - }) : super(DatabaseLayoutState.initial(viewId, databaseLayout)) { + }) : layoutService = DatabaseLayoutBackendService(viewId), + super(DatabaseLayoutState.initial(viewId, databaseLayout)) { on( (event, emit) async { event.when( initial: () {}, updateLayout: (DatabaseLayoutPB layout) { - DatabaseViewBackendService.updateLayout( - viewId: viewId, + layoutService.updateLayout( + fieldId: viewId, layout: layout, ); emit(state.copyWith(databaseLayout: layout)); @@ -31,7 +34,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_view/application/layout/layout_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_listener.dart new file mode 100644 index 0000000000..c3683d921e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_listener.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; + +/// Listener for database layout changes. +class DatabaseLayoutListener { + final String viewId; + PublishNotifier>? _layoutNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + DatabaseLayoutListener(this.viewId); + + void start({ + required void Function(Either) + onLayoutChanged, + }) { + _layoutNotifier?.addPublishListener(onLayoutChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateDatabaseLayout: + result.fold( + (payload) => _layoutNotifier?.value = + left(DatabaseLayoutMetaPB.fromBuffer(payload).layout), + (error) => _layoutNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _layoutNotifier?.dispose(); + _layoutNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart new file mode 100644 index 0000000000..5d517945fb --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart @@ -0,0 +1,48 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:dartz/dartz.dart'; + +class DatabaseLayoutBackendService { + final String viewId; + + DatabaseLayoutBackendService(this.viewId); + + Future> updateLayout({ + required String fieldId, + required DatabaseLayoutPB layout, + }) { + final payload = UpdateViewPayloadPB.create() + ..viewId = viewId + ..layout = viewLayoutFromDatabaseLayout(layout); + + return FolderEventUpdateView(payload).send(); + } +} + +ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) { + switch (databaseLayout) { + case DatabaseLayoutPB.Board: + return ViewLayoutPB.Board; + case DatabaseLayoutPB.Calendar: + return ViewLayoutPB.Calendar; + case DatabaseLayoutPB.Grid: + return ViewLayoutPB.Grid; + default: + throw UnimplementedError; + } +} + +DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) { + switch (viewLayout) { + case ViewLayoutPB.Board: + return DatabaseLayoutPB.Board; + case ViewLayoutPB.Calendar: + return DatabaseLayoutPB.Calendar; + case ViewLayoutPB.Grid: + return DatabaseLayoutPB.Grid; + default: + throw UnimplementedError; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/layout_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart similarity index 79% rename from frontend/appflowy_flutter/lib/plugins/database/domain/layout_setting_listener.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart index fd213f74be..fc1553a573 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/layout_setting_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart @@ -1,21 +1,19 @@ import 'dart:typed_data'; import 'package:appflowy/core/notification/grid_notification.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:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; -typedef LayoutSettingsValue = FlowyResult; +typedef LayoutSettingsValue = Either; class DatabaseLayoutSettingListener { - DatabaseLayoutSettingListener(this.viewId); - final String viewId; - PublishNotifier>? _settingNotifier = PublishNotifier(); DatabaseNotificationListener? _listener; + DatabaseLayoutSettingListener(this.viewId); void start({ required void Function(LayoutSettingsValue) @@ -30,14 +28,14 @@ class DatabaseLayoutSettingListener { void _handler( DatabaseNotification ty, - FlowyResult result, + Either result, ) { switch (ty) { case DatabaseNotification.DidUpdateLayoutSettings: result.fold( (payload) => _settingNotifier?.value = - FlowyResult.success(DatabaseLayoutSettingPB.fromBuffer(payload)), - (error) => _settingNotifier?.value = FlowyResult.failure(error), + left(DatabaseLayoutSettingPB.fromBuffer(payload)), + (error) => _settingNotifier?.value = right(error), ); break; default: diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart new file mode 100644 index 0000000000..f06b74d77a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart @@ -0,0 +1,163 @@ +import 'package:appflowy/plugins/database_view/application/field/field_listener.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'row_meta_listener.dart'; + +part 'row_banner_bloc.freezed.dart'; + +class RowBannerBloc extends Bloc { + final String viewId; + final RowBackendService _rowBackendSvc; + final RowMetaListener _metaListener; + SingleFieldListener? _fieldListener; + + RowBannerBloc({ + required this.viewId, + required RowMetaPB rowMeta, + }) : _rowBackendSvc = RowBackendService(viewId: viewId), + _metaListener = RowMetaListener(rowMeta.id), + super(RowBannerState.initial(rowMeta)) { + on( + (event, emit) async { + event.when( + initial: () async { + _loadPrimaryField(); + await _listenRowMeteChanged(); + }, + didReceiveRowMeta: (RowMetaPB rowMeta) { + emit( + state.copyWith( + rowMeta: rowMeta, + ), + ); + }, + setCover: (String coverURL) { + _updateMeta(coverURL: coverURL); + }, + setIcon: (String iconURL) { + _updateMeta(iconURL: iconURL); + }, + didReceiveFieldUpdate: (updatedField) { + emit( + state.copyWith( + primaryField: updatedField, + loadingState: const LoadingState.finish(), + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + await _metaListener.stop(); + await _fieldListener?.stop(); + _fieldListener = null; + + return super.close(); + } + + Future _loadPrimaryField() async { + final fieldOrError = + await FieldBackendService.getPrimaryField(viewId: viewId); + fieldOrError.fold( + (primaryField) { + if (!isClosed) { + _fieldListener = SingleFieldListener(fieldId: primaryField.id); + _fieldListener?.start( + onFieldChanged: (updatedField) { + if (!isClosed) { + add(RowBannerEvent.didReceiveFieldUpdate(updatedField)); + } + }, + ); + add(RowBannerEvent.didReceiveFieldUpdate(primaryField)); + } + }, + (r) => Log.error(r), + ); + } + + /// Listen the changes of the row meta and then update the banner + Future _listenRowMeteChanged() async { + _metaListener.start( + callback: (rowMeta) { + add(RowBannerEvent.didReceiveRowMeta(rowMeta)); + }, + ); + } + + /// Update the meta of the row and the view + Future _updateMeta({ + String? iconURL, + String? coverURL, + }) async { + // Most of the time, the result is success, so we don't need to handle it. + await _rowBackendSvc + .updateMeta( + iconURL: iconURL, + coverURL: coverURL, + rowId: state.rowMeta.id, + ) + .then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + + // Set the icon and cover of the view + ViewBackendService.updateView( + viewId: viewId, + iconURL: iconURL, + coverURL: coverURL, + ).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + } +} + +@freezed +class RowBannerEvent with _$RowBannerEvent { + const factory RowBannerEvent.initial() = _Initial; + const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) = + _DidReceiveRowMeta; + const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) = + _DidReceiveFieldUdate; + const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon; + const factory RowBannerEvent.setCover(String coverURL) = _SetCover; +} + +@freezed +class RowBannerState with _$RowBannerState { + const factory RowBannerState({ + ViewPB? view, + FieldPB? primaryField, + required RowMetaPB rowMeta, + required LoadingState loadingState, + }) = _RowBannerState; + + factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState( + rowMeta: rowMetaPB, + loadingState: const LoadingState.loading(), + ); +} + +@freezed +class LoadingState with _$LoadingState { + const factory LoadingState.loading() = _Loading; + const factory LoadingState.finish() = _Finish; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart new file mode 100644 index 0000000000..83656a7ff9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -0,0 +1,367 @@ +import 'dart:collection'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../cell/cell_service.dart'; +import '../field/field_controller.dart'; +import 'row_list.dart'; +import 'row_service.dart'; +part 'row_cache.freezed.dart'; + +typedef RowUpdateCallback = void Function(); + +abstract class RowFieldsDelegate { + void onFieldsChanged(void Function(List) callback); +} + +abstract mixin class RowCacheDelegate { + UnmodifiableListView get fields; + void onRowDispose(); +} + +/// Cache the rows in memory +/// Insert / delete / update row +/// +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information. + +class RowCache { + final String viewId; + + /// _rows contains the current block's rows + /// Use List to reverse the order of the GridRow. + final RowList _rowList = RowList(); + + final CellCache _cellCache; + final RowCacheDelegate _delegate; + final RowChangesetNotifier _rowChangeReasonNotifier; + + /// Returns a unmodifiable list of RowInfo + UnmodifiableListView get rowInfos { + final visibleRows = [..._rowList.rows]; + return UnmodifiableListView(visibleRows); + } + + /// Returns a unmodifiable map of rowId to RowInfo + UnmodifiableMapView get rowByRowId { + return UnmodifiableMapView(_rowList.rowInfoByRowId); + } + + CellCache get cellCache => _cellCache; + + RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason; + + RowCache({ + required this.viewId, + required RowFieldsDelegate fieldsDelegate, + required RowCacheDelegate cacheDelegate, + }) : _cellCache = CellCache(viewId: viewId), + _rowChangeReasonNotifier = RowChangesetNotifier(), + _delegate = cacheDelegate { + // + fieldsDelegate.onFieldsChanged((fieldInfos) { + for (final fieldInfo in fieldInfos) { + _cellCache.removeCellWithFieldId(fieldInfo.id); + } + _rowChangeReasonNotifier + .receive(const RowsChangedReason.fieldDidChange()); + }); + } + + RowInfo? getRow(RowId rowId) { + return _rowList.get(rowId); + } + + void setInitialRows(List rows) { + for (final row in rows) { + final rowInfo = buildGridRow(row); + _rowList.add(rowInfo); + } + } + + Future dispose() async { + _delegate.onRowDispose(); + _rowChangeReasonNotifier.dispose(); + await _cellCache.dispose(); + } + + void applyRowsChanged(RowsChangePB changeset) { + _deleteRows(changeset.deletedRows); + _insertRows(changeset.insertedRows); + _updateRows(changeset.updatedRows); + } + + void applyRowsVisibility(RowsVisibilityChangePB changeset) { + _hideRows(changeset.invisibleRows); + _showRows(changeset.visibleRows); + } + + void reorderAllRows(List rowIds) { + _rowList.reorderWithRowIds(rowIds); + _rowChangeReasonNotifier.receive(const RowsChangedReason.reorderRows()); + } + + void reorderSingleRow(ReorderSingleRowPB reorderRow) { + final rowInfo = _rowList.get(reorderRow.rowId); + if (rowInfo != null) { + _rowList.moveRow( + reorderRow.rowId, + reorderRow.oldIndex, + reorderRow.newIndex, + ); + _rowChangeReasonNotifier.receive( + RowsChangedReason.reorderSingleRow( + reorderRow, + rowInfo, + ), + ); + } + } + + void _deleteRows(List deletedRowIds) { + for (final rowId in deletedRowIds) { + final deletedRow = _rowList.remove(rowId); + if (deletedRow != null) { + _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow)); + } + } + } + + void _insertRows(List insertRows) { + for (final insertedRow in insertRows) { + final insertedIndex = + _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); + if (insertedIndex != null) { + _rowChangeReasonNotifier + .receive(RowsChangedReason.insert(insertedIndex)); + } + } + } + + void _updateRows(List updatedRows) { + if (updatedRows.isEmpty) return; + final List updatedList = []; + for (final updatedRow in updatedRows) { + for (final fieldId in updatedRow.fieldIds) { + final key = CellCacheKey( + fieldId: fieldId, + rowId: updatedRow.rowId, + ); + _cellCache.remove(key); + } + if (updatedRow.hasRowMeta()) { + updatedList.add(updatedRow.rowMeta); + } + } + + final updatedIndexs = + _rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId)); + + if (updatedIndexs.isNotEmpty) { + _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); + } + } + + void _hideRows(List invisibleRows) { + for (final rowId in invisibleRows) { + final deletedRow = _rowList.remove(rowId); + if (deletedRow != null) { + _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow)); + } + } + } + + void _showRows(List visibleRows) { + for (final insertedRow in visibleRows) { + final insertedIndex = + _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); + if (insertedIndex != null) { + _rowChangeReasonNotifier + .receive(RowsChangedReason.insert(insertedIndex)); + } + } + } + + void onRowsChanged(void Function(RowsChangedReason) onRowChanged) { + _rowChangeReasonNotifier.addListener(() { + onRowChanged(_rowChangeReasonNotifier.reason); + }); + } + + RowUpdateCallback addListener({ + required RowId rowId, + void Function(CellContextByFieldId, RowsChangedReason)? onCellUpdated, + bool Function()? listenWhen, + }) { + listenerHandler() async { + if (listenWhen != null && listenWhen() == false) { + return; + } + + notifyUpdate() { + if (onCellUpdated != null) { + final rowInfo = _rowList.get(rowId); + if (rowInfo != null) { + final CellContextByFieldId cellDataMap = _makeGridCells( + rowInfo.rowMeta, + ); + onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason); + } + } + } + + _rowChangeReasonNotifier.reason.whenOrNull( + update: (indexs) { + if (indexs[rowId] != null) notifyUpdate(); + }, + fieldDidChange: () => notifyUpdate(), + ); + } + + _rowChangeReasonNotifier.addListener(listenerHandler); + return listenerHandler; + } + + void removeRowListener(VoidCallback callback) { + _rowChangeReasonNotifier.removeListener(callback); + } + + CellContextByFieldId loadGridCells(RowMetaPB rowMeta) { + final rowInfo = _rowList.get(rowMeta.id); + if (rowInfo == null) { + _loadRow(rowMeta.id); + } + return _makeGridCells(rowMeta); + } + + Future _loadRow(RowId rowId) async { + final payload = RowIdPB.create() + ..viewId = viewId + ..rowId = rowId; + + final result = await DatabaseEventGetRowMeta(payload).send(); + result.fold( + (rowMetaPB) { + final rowInfo = _rowList.get(rowMetaPB.id); + final rowIndex = _rowList.indexOfRow(rowMetaPB.id); + if (rowInfo != null && rowIndex != null) { + final updatedRowInfo = rowInfo.copyWith(rowMeta: rowMetaPB); + _rowList.remove(rowMetaPB.id); + _rowList.insert(rowIndex, updatedRowInfo); + + final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); + updatedIndexs[rowMetaPB.id] = UpdatedIndex( + index: rowIndex, + rowId: rowMetaPB.id, + ); + + _rowChangeReasonNotifier + .receive(RowsChangedReason.update(updatedIndexs)); + } + }, + (err) => Log.error(err), + ); + } + + CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) { + // ignore: prefer_collection_literals + final cellContextMap = CellContextByFieldId(); + for (final field in _delegate.fields) { + if (field.visibility) { + cellContextMap[field.id] = DatabaseCellContext( + rowMeta: rowMeta, + viewId: viewId, + fieldInfo: field, + ); + } + } + return cellContextMap; + } + + RowInfo buildGridRow(RowMetaPB rowMetaPB) { + return RowInfo( + viewId: viewId, + fields: _delegate.fields, + rowId: rowMetaPB.id, + rowMeta: rowMetaPB, + ); + } +} + +class RowChangesetNotifier extends ChangeNotifier { + RowsChangedReason reason = const InitialListState(); + + RowChangesetNotifier(); + + void receive(RowsChangedReason newReason) { + reason = newReason; + reason.map( + insert: (_) => notifyListeners(), + delete: (_) => notifyListeners(), + update: (_) => notifyListeners(), + fieldDidChange: (_) => notifyListeners(), + initial: (_) {}, + reorderRows: (_) => notifyListeners(), + reorderSingleRow: (_) => notifyListeners(), + ); + } +} + +@unfreezed +class RowInfo with _$RowInfo { + factory RowInfo({ + required String rowId, + required String viewId, + required UnmodifiableListView fields, + required RowMetaPB rowMeta, + }) = _RowInfo; +} + +typedef InsertedIndexs = List; +typedef DeletedIndexs = List; +// key: id of the row +// value: UpdatedIndex +typedef UpdatedIndexMap = LinkedHashMap; + +@freezed +class RowsChangedReason with _$RowsChangedReason { + const factory RowsChangedReason.insert(InsertedIndex item) = _Insert; + const factory RowsChangedReason.delete(DeletedIndex item) = _Delete; + const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update; + const factory RowsChangedReason.fieldDidChange() = _FieldDidChange; + const factory RowsChangedReason.initial() = InitialListState; + const factory RowsChangedReason.reorderRows() = _ReorderRows; + const factory RowsChangedReason.reorderSingleRow( + ReorderSingleRowPB reorderRow, + RowInfo rowInfo, + ) = _ReorderSingleRow; +} + +class InsertedIndex { + final int index; + final RowId rowId; + InsertedIndex({ + required this.index, + required this.rowId, + }); +} + +class DeletedIndex { + final int index; + final RowInfo rowInfo; + DeletedIndex({ + required this.index, + required this.rowInfo, + }); +} + +class UpdatedIndex { + final int index; + final RowId rowId; + UpdatedIndex({ + required this.index, + required this.rowId, + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart new file mode 100644 index 0000000000..4dacd7310a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart @@ -0,0 +1,45 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:flutter/material.dart'; +import '../cell/cell_service.dart'; +import 'row_cache.dart'; + +typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason); + +class RowController { + final RowMetaPB rowMeta; + final String? groupId; + final String viewId; + final List _onRowChangedListeners = []; + final RowCache _rowCache; + + get cellCache => _rowCache.cellCache; + + get rowId => rowMeta.id; + + RowController({ + required this.rowMeta, + required this.viewId, + required RowCache rowCache, + this.groupId, + }) : _rowCache = rowCache; + + CellContextByFieldId loadData() { + return _rowCache.loadGridCells(rowMeta); + } + + void addListener({OnRowChanged? onRowChanged}) { + final fn = _rowCache.addListener( + rowId: rowMeta.id, + onCellUpdated: onRowChanged, + ); + + // Add the listener to the list so that we can remove it later. + _onRowChangedListeners.add(fn); + } + + 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_view/application/row/row_list.dart similarity index 88% rename from frontend/appflowy_flutter/lib/plugins/database/application/row/row_list.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart index 00b0745448..4fd489dd98 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart @@ -1,8 +1,5 @@ import 'dart:collection'; -import 'dart:math'; - import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; - import 'row_cache.dart'; import 'row_service.dart'; @@ -103,7 +100,7 @@ class RowList { final List newRows = []; final DeletedIndexs deletedIndex = []; final Map deletedRowByRowId = { - for (final rowId in rowIds) rowId: rowId, + for (var rowId in rowIds) rowId: rowId }; _rowInfos.asMap().forEach((index, RowInfo rowInfo) { @@ -118,23 +115,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 +160,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/domain/row_meta_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart similarity index 91% rename from frontend/appflowy_flutter/lib/plugins/database/domain/row_meta_listener.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart index 1b7e66fd75..d696240e84 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/row_meta_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart @@ -2,19 +2,17 @@ import 'dart:typed_data'; import 'package:appflowy/core/notification/grid_notification.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_result/appflowy_result.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; typedef RowMetaCallback = void Function(RowMetaPB); class RowMetaListener { - RowMetaListener(this.rowId); - final String rowId; - RowMetaCallback? _callback; DatabaseNotificationListener? _listener; + RowMetaListener(this.rowId); void start({required RowMetaCallback callback}) { _callback = callback; @@ -26,7 +24,7 @@ class RowMetaListener { void _handler( DatabaseNotification ty, - FlowyResult result, + Either result, ) { switch (ty) { case DatabaseNotification.DidUpdateRowMeta: diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart new file mode 100644 index 0000000000..8668341f78 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart @@ -0,0 +1,79 @@ +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; + +typedef RowId = String; + +class RowBackendService { + final String viewId; + + RowBackendService({ + required this.viewId, + }); + + Future> createRowAfterRow(RowId rowId) { + final payload = CreateRowPayloadPB.create() + ..viewId = viewId + ..startRowId = rowId; + + return DatabaseEventCreateRow(payload).send(); + } + + Future> getRow(RowId rowId) { + final payload = RowIdPB.create() + ..viewId = viewId + ..rowId = rowId; + + return DatabaseEventGetRow(payload).send(); + } + + Future> getRowMeta(RowId rowId) { + final payload = RowIdPB.create() + ..viewId = viewId + ..rowId = rowId; + + return DatabaseEventGetRowMeta(payload).send(); + } + + Future> updateMeta({ + required String rowId, + String? iconURL, + String? coverURL, + }) { + final payload = UpdateRowMetaChangesetPB.create() + ..viewId = viewId + ..id = rowId; + + if (iconURL != null) { + payload.iconUrl = iconURL; + } + if (coverURL != null) { + payload.coverUrl = coverURL; + } + + return DatabaseEventUpdateRowMeta(payload).send(); + } + + Future> deleteRow(RowId rowId) { + final payload = RowIdPB.create() + ..viewId = viewId + ..rowId = rowId; + + return DatabaseEventDeleteRow(payload).send(); + } + + Future> duplicateRow({ + required RowId rowId, + String? groupId, + }) { + final payload = RowIdPB.create() + ..viewId = viewId + ..rowId = rowId; + if (groupId != null) { + payload.groupId = groupId; + } + + return DatabaseEventDuplicateRow(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart new file mode 100644 index 0000000000..93a7df4e11 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart @@ -0,0 +1,89 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import '../group/group_service.dart'; + +part 'group_bloc.freezed.dart'; + +class DatabaseGroupBloc extends Bloc { + final FieldController _fieldController; + final GroupBackendService _groupBackendSvc; + Function(List)? _onFieldsFn; + + DatabaseGroupBloc({ + required String viewId, + required FieldController fieldController, + }) : _fieldController = fieldController, + _groupBackendSvc = GroupBackendService(viewId), + super(DatabaseGroupState.initial(viewId, fieldController.fieldInfos)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveFieldUpdate: (fieldContexts) { + emit(state.copyWith(fieldContexts: fieldContexts)); + }, + setGroupByField: (String fieldId, FieldType fieldType) async { + final result = await _groupBackendSvc.groupByField( + fieldId: fieldId, + ); + result.fold((l) => null, (err) => Log.error(err)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onFieldsFn != null) { + _fieldController.removeListener(onFieldsListener: _onFieldsFn!); + _onFieldsFn = null; + } + return super.close(); + } + + void _startListening() { + _onFieldsFn = (fieldContexts) => + add(DatabaseGroupEvent.didReceiveFieldUpdate(fieldContexts)); + _fieldController.addListener( + onReceiveFields: _onFieldsFn, + listenWhen: () => !isClosed, + ); + } +} + +@freezed +class DatabaseGroupEvent with _$DatabaseGroupEvent { + const factory DatabaseGroupEvent.initial() = _Initial; + const factory DatabaseGroupEvent.setGroupByField( + String fieldId, + FieldType fieldType, + ) = _DatabaseGroupEvent; + const factory DatabaseGroupEvent.didReceiveFieldUpdate( + List fields, + ) = _DidReceiveFieldUpdate; +} + +@freezed +class DatabaseGroupState with _$DatabaseGroupState { + const factory DatabaseGroupState({ + required String viewId, + required List fieldContexts, + }) = _DatabaseGroupState; + + factory DatabaseGroupState.initial( + String viewId, + List fieldContexts, + ) => + DatabaseGroupState( + viewId: viewId, + fieldContexts: fieldContexts, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart new file mode 100644 index 0000000000..117dd1c3aa --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart @@ -0,0 +1,98 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import '../field/field_service.dart'; + +part 'property_bloc.freezed.dart'; + +class DatabasePropertyBloc + extends Bloc { + final FieldController _fieldController; + Function(List)? _onFieldsFn; + + DatabasePropertyBloc({ + required String viewId, + required FieldController fieldController, + }) : _fieldController = fieldController, + super( + DatabasePropertyState.initial(viewId, fieldController.fieldInfos), + ) { + on( + (event, emit) async { + await event.map( + initial: (_Initial value) { + _startListening(); + }, + setFieldVisibility: (_SetFieldVisibility value) async { + final fieldBackendSvc = + FieldBackendService(viewId: viewId, fieldId: value.fieldId); + final result = + await fieldBackendSvc.updateField(visibility: value.visibility); + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }, + didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) { + emit(state.copyWith(fieldContexts: value.fields)); + }, + moveField: (_MoveField value) { + // + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onFieldsFn != null) { + _fieldController.removeListener(onFieldsListener: _onFieldsFn!); + _onFieldsFn = null; + } + return super.close(); + } + + void _startListening() { + _onFieldsFn = + (fields) => add(DatabasePropertyEvent.didReceiveFieldUpdate(fields)); + _fieldController.addListener( + onReceiveFields: _onFieldsFn, + listenWhen: () => !isClosed, + ); + } +} + +@freezed +class DatabasePropertyEvent with _$DatabasePropertyEvent { + const factory DatabasePropertyEvent.initial() = _Initial; + const factory DatabasePropertyEvent.setFieldVisibility( + String fieldId, + bool visibility, + ) = _SetFieldVisibility; + const factory DatabasePropertyEvent.didReceiveFieldUpdate( + List fields, + ) = _DidReceiveFieldUpdate; + const factory DatabasePropertyEvent.moveField(int fromIndex, int toIndex) = + _MoveField; +} + +@freezed +class DatabasePropertyState with _$DatabasePropertyState { + const factory DatabasePropertyState({ + required String viewId, + required List fieldContexts, + }) = _GridPropertyState; + + factory DatabasePropertyState.initial( + String viewId, + List fieldContexts, + ) => + DatabasePropertyState( + viewId: viewId, + fieldContexts: fieldContexts, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart new file mode 100644 index 0000000000..f1b827df92 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart @@ -0,0 +1,61 @@ +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'setting_listener.dart'; +import 'setting_service.dart'; + +typedef OnError = void Function(FlowyError); +typedef OnSettingUpdated = void Function(DatabaseViewSettingPB); + +class SettingController { + final String viewId; + final SettingBackendService _settingBackendSvc; + final DatabaseSettingListener _listener; + OnSettingUpdated? _onSettingUpdated; + OnError? _onError; + DatabaseViewSettingPB? _setting; + DatabaseViewSettingPB? get setting => _setting; + + SettingController({ + required this.viewId, + }) : _settingBackendSvc = SettingBackendService(viewId: viewId), + _listener = DatabaseSettingListener(viewId: viewId) { + // Load setting + _settingBackendSvc.getSetting().then((result) { + result.fold( + (newSetting) => updateSetting(newSetting), + (err) => _onError?.call(err), + ); + }); + + // Listen on the setting changes + _listener.start( + onSettingUpdated: (result) { + result.fold( + (newSetting) => updateSetting(newSetting), + (err) => _onError?.call(err), + ); + }, + ); + } + + void startListening({ + required OnSettingUpdated onSettingUpdated, + required OnError onError, + }) { + assert(_onSettingUpdated == null, 'Should call once'); + assert(_onError == null, 'Should call once'); + _onSettingUpdated = onSettingUpdated; + _onError = onError; + } + + void updateSetting(DatabaseViewSettingPB newSetting) { + _setting = newSetting; + _onSettingUpdated?.call(newSetting); + } + + void dispose() { + _onSettingUpdated = null; + _onError = null; + _listener.stop(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_listener.dart similarity index 75% rename from frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_listener.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_listener.dart index 34dfba53d4..b9be9becf3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_listener.dart @@ -1,24 +1,22 @@ import 'dart:typed_data'; import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_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 UpdateSettingNotifiedValue - = FlowyResult; +typedef UpdateSettingNotifiedValue = Either; class DatabaseSettingListener { - DatabaseSettingListener({required this.viewId}); - final String viewId; - DatabaseNotificationListener? _listener; PublishNotifier? _updateSettingNotifier = PublishNotifier(); + DatabaseSettingListener({required this.viewId}); + void start({ required void Function(UpdateSettingNotifiedValue) onSettingUpdated, }) { @@ -27,17 +25,14 @@ class DatabaseSettingListener { DatabaseNotificationListener(objectId: viewId, handler: _handler); } - void _handler( - DatabaseNotification ty, - FlowyResult result, - ) { + void _handler(DatabaseNotification ty, Either result) { switch (ty) { case DatabaseNotification.DidUpdateSettings: result.fold( - (payload) => _updateSettingNotifier?.value = FlowyResult.success( + (payload) => _updateSettingNotifier?.value = left( DatabaseViewSettingPB.fromBuffer(payload), ), - (error) => _updateSettingNotifier?.value = FlowyResult.failure(error), + (error) => _updateSettingNotifier?.value = right(error), ); break; default: diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart similarity index 80% rename from frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_service.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart index d34fab1aea..436acad0b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart @@ -1,15 +1,15 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; class SettingBackendService { - const SettingBackendService({required this.viewId}); - final String viewId; - Future> getSetting() { + const SettingBackendService({required this.viewId}); + + Future> getSetting() { final payload = DatabaseViewIdPB.create()..value = viewId; return DatabaseEventGetDatabaseSetting(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_listener.dart new file mode 100644 index 0000000000..64ae13787f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_listener.dart @@ -0,0 +1,51 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; + +typedef SortNotifiedValue = Either; + +class SortsListener { + final String viewId; + PublishNotifier? _notifier = PublishNotifier(); + DatabaseNotificationListener? _listener; + + SortsListener({required this.viewId}); + + void start({ + required void Function(SortNotifiedValue) onSortChanged, + }) { + _notifier?.addPublishListener(onSortChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateSort: + result.fold( + (payload) => _notifier?.value = + left(SortChangesetNotificationPB.fromBuffer(payload)), + (error) => _notifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _notifier?.dispose(); + _notifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart new file mode 100644 index 0000000000..79be9083a8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart @@ -0,0 +1,116 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:dartz/dartz.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-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; + +class SortBackendService { + final String viewId; + + SortBackendService({required this.viewId}); + + Future, FlowyError>> getAllSorts() { + final payload = DatabaseViewIdPB()..value = viewId; + + return DatabaseEventGetAllSorts(payload).send().then((result) { + return result.fold( + (repeated) => left(repeated.items), + (r) => right(r), + ); + }); + } + + Future> updateSort({ + required String fieldId, + required String sortId, + required FieldType fieldType, + required SortConditionPB condition, + }) { + final insertSortPayload = UpdateSortPayloadPB.create() + ..fieldId = fieldId + ..fieldType = fieldType + ..viewId = viewId + ..condition = condition + ..sortId = sortId; + + final payload = DatabaseSettingChangesetPB.create() + ..viewId = viewId + ..updateSort = insertSortPayload; + return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { + return result.fold( + (l) => left(l), + (err) { + Log.error(err); + return right(err); + }, + ); + }); + } + + Future> insertSort({ + required String fieldId, + required FieldType fieldType, + required SortConditionPB condition, + }) { + final insertSortPayload = UpdateSortPayloadPB.create() + ..fieldId = fieldId + ..fieldType = fieldType + ..viewId = viewId + ..condition = condition; + + final payload = DatabaseSettingChangesetPB.create() + ..viewId = viewId + ..updateSort = insertSortPayload; + return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { + return result.fold( + (l) => left(l), + (err) { + Log.error(err); + return right(err); + }, + ); + }); + } + + Future> deleteSort({ + required String fieldId, + required String sortId, + required FieldType fieldType, + }) { + final deleteSortPayload = DeleteSortPayloadPB.create() + ..fieldId = fieldId + ..sortId = sortId + ..viewId = viewId + ..fieldType = fieldType; + + final payload = DatabaseSettingChangesetPB.create() + ..viewId = viewId + ..deleteSort = deleteSortPayload; + + return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { + return result.fold( + (l) => left(l), + (err) { + Log.error(err); + return right(err); + }, + ); + }); + } + + Future> deleteAllSorts() { + final payload = DatabaseViewIdPB(value: viewId); + return DatabaseEventDeleteAllSorts(payload).send().then((result) { + return result.fold( + (l) => left(l), + (err) { + Log.error(err); + return right(err); + }, + ); + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart new file mode 100644 index 0000000000..4b4697add2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart @@ -0,0 +1,290 @@ +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.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-folder2/view.pb.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'database_controller.dart'; +import 'database_view_service.dart'; + +part 'tar_bar_bloc.freezed.dart'; + +class GridTabBarBloc extends Bloc { + GridTabBarBloc({ + bool isInlineView = false, + required ViewPB view, + }) : super(GridTabBarState.initial(view)) { + on( + (event, emit) async { + event.when( + initial: () { + _listenInlineViewChanged(); + _loadChildView(); + }, + didLoadChildViews: (List childViews) { + emit( + state.copyWith( + tabBars: [ + ...state.tabBars, + ...childViews.map( + (newChildView) => TarBar(view: newChildView), + ), + ], + tabBarControllerByViewId: _extendsTabBarController(childViews), + ), + ); + }, + selectView: (String viewId) { + final index = + state.tabBars.indexWhere((element) => element.viewId == viewId); + if (index != -1) { + emit( + state.copyWith(selectedIndex: index), + ); + } + }, + createView: (action) { + _createLinkedView(action.name, action.layoutType); + }, + deleteView: (String viewId) async { + final result = await ViewBackendService.delete(viewId: viewId); + result.fold( + (l) {}, + (r) => Log.error(r), + ); + }, + renameView: (String viewId, String newName) { + ViewBackendService.updateView(viewId: viewId, name: newName); + }, + didUpdateChildViews: (updatePB) async { + if (updatePB.createChildViews.isNotEmpty) { + final allTabBars = [ + ...state.tabBars, + ...updatePB.createChildViews.map((e) => TarBar(view: e)) + ]; + emit( + state.copyWith( + tabBars: allTabBars, + selectedIndex: state.tabBars.length, + tabBarControllerByViewId: + _extendsTabBarController(updatePB.createChildViews), + ), + ); + } + + if (updatePB.deleteChildViews.isNotEmpty) { + final allTabBars = [...state.tabBars]; + final tabBarControllerByViewId = { + ...state.tabBarControllerByViewId + }; + var newSelectedIndex = state.selectedIndex; + for (final viewId in updatePB.deleteChildViews) { + final index = allTabBars.indexWhere( + (element) => element.viewId == viewId, + ); + if (index != -1) { + final tarBar = allTabBars.removeAt(index); + // Dispose the controller when the tab is removed. + final controller = + tabBarControllerByViewId.remove(tarBar.viewId); + controller?.dispose(); + } + + if (index == state.selectedIndex) { + if (index > 0 && allTabBars.isNotEmpty) { + newSelectedIndex = index - 1; + } + } + } + emit( + state.copyWith( + tabBars: allTabBars, + selectedIndex: newSelectedIndex, + tabBarControllerByViewId: tabBarControllerByViewId, + ), + ); + } + }, + viewDidUpdate: (ViewPB updatedView) { + final index = state.tabBars.indexWhere( + (element) => element.viewId == updatedView.id, + ); + if (index != -1) { + final allTabBars = [...state.tabBars]; + final updatedTabBar = TarBar(view: updatedView); + allTabBars[index] = updatedTabBar; + emit(state.copyWith(tabBars: allTabBars)); + } + }, + ); + }, + ); + } + + @override + Future close() async { + for (final tabBar in state.tabBars) { + await state.tabBarControllerByViewId[tabBar.viewId]?.dispose(); + } + return super.close(); + } + + void _listenInlineViewChanged() { + final controller = state.tabBarControllerByViewId[state.parentView.id]; + controller?.onViewUpdated = (newView) { + add(GridTabBarEvent.viewDidUpdate(newView)); + }; + + // Only listen the child view changes when the parent view is inline. + controller?.onViewChildViewChanged = (update) { + add(GridTabBarEvent.didUpdateChildViews(update)); + }; + } + + /// Create tab bar controllers for the new views and return the updated map. + Map _extendsTabBarController( + List newViews, + ) { + final tabBarControllerByViewId = {...state.tabBarControllerByViewId}; + for (final view in newViews) { + final controller = DatabaseTarBarController(view: view); + controller.onViewUpdated = (newView) { + add(GridTabBarEvent.viewDidUpdate(newView)); + }; + + tabBarControllerByViewId[view.id] = controller; + } + return tabBarControllerByViewId; + } + + Future _createLinkedView(String name, ViewLayoutPB layoutType) async { + final viewId = state.parentView.id; + final databaseIdOrError = + await DatabaseViewBackendService(viewId: viewId).getDatabaseId(); + databaseIdOrError.fold( + (databaseId) async { + final linkedViewOrError = + await ViewBackendService.createDatabaseLinkedView( + parentViewId: viewId, + databaseId: databaseId, + layoutType: layoutType, + name: name, + ); + + linkedViewOrError.fold( + (linkedView) {}, + (err) => Log.error(err), + ); + }, + (r) => Log.error(r), + ); + } + + Future _loadChildView() async { + ViewBackendService.getChildViews(viewId: state.parentView.id) + .then((viewsOrFail) { + if (isClosed) { + return; + } + viewsOrFail.fold( + (views) => add(GridTabBarEvent.didLoadChildViews(views)), + (err) => Log.error(err), + ); + }); + } +} + +@freezed +class GridTabBarEvent with _$GridTabBarEvent { + const factory GridTabBarEvent.initial() = _Initial; + const factory GridTabBarEvent.didLoadChildViews( + List childViews, + ) = _DidLoadChildViews; + const factory GridTabBarEvent.selectView(String viewId) = _DidSelectView; + const factory GridTabBarEvent.createView(AddButtonAction action) = + _CreateView; + const factory GridTabBarEvent.renameView(String viewId, String newName) = + _RenameView; + const factory GridTabBarEvent.deleteView(String viewId) = _DeleteView; + const factory GridTabBarEvent.didUpdateChildViews( + ChildViewUpdatePB updatePB, + ) = _DidUpdateChildViews; + const factory GridTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate; +} + +@freezed +class GridTabBarState with _$GridTabBarState { + const factory GridTabBarState({ + required ViewPB parentView, + required int selectedIndex, + required List tabBars, + required Map tabBarControllerByViewId, + }) = _GridTabBarState; + + factory GridTabBarState.initial(ViewPB view) { + final tabBar = TarBar(view: view); + return GridTabBarState( + parentView: view, + selectedIndex: 0, + tabBars: [tabBar], + tabBarControllerByViewId: { + view.id: DatabaseTarBarController( + view: view, + ) + }, + ); + } +} + +class TarBar extends Equatable { + final ViewPB view; + final DatabaseTabBarItemBuilder _builder; + + String get viewId => view.id; + DatabaseTabBarItemBuilder get builder => _builder; + ViewLayoutPB get layout => view.layout; + + TarBar({ + required this.view, + }) : _builder = view.tarBarItem(); + + @override + List get props => [view.hashCode]; +} + +typedef OnViewUpdated = void Function(ViewPB newView); +typedef OnViewChildViewChanged = void Function( + ChildViewUpdatePB childViewUpdate, +); + +class DatabaseTarBarController { + ViewPB view; + final DatabaseController controller; + final ViewListener viewListener; + OnViewUpdated? onViewUpdated; + OnViewChildViewChanged? onViewChildViewChanged; + + DatabaseTarBarController({ + required this.view, + }) : controller = DatabaseController(view: view), + viewListener = ViewListener(viewId: view.id) { + viewListener.start( + onViewChildViewsUpdated: (update) { + onViewChildViewChanged?.call(update); + }, + onViewUpdated: (newView) { + view = newView; + onViewUpdated?.call(newView); + }, + ); + } + + Future dispose() async { + await viewListener.stop(); + await controller.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart new file mode 100644 index 0000000000..b961750068 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/log.dart'; +import '../defines.dart'; +import '../field/field_controller.dart'; +import '../row/row_cache.dart'; +import 'view_listener.dart'; + +class DatabaseViewCallbacks { + /// Will get called when number of rows were changed that includes + /// update/delete/insert rows. The [onNumOfRowsChanged] will return all + /// the rows of the current database + final OnNumOfRowsChanged? onNumOfRowsChanged; + + // Will get called when creating new rows + final OnRowsCreated? onRowsCreated; + + /// Will get called when rows were updated + final OnRowsUpdated? onRowsUpdated; + + /// Will get called when number of rows were deleted + final OnRowsDeleted? onRowsDeleted; + + const DatabaseViewCallbacks({ + this.onNumOfRowsChanged, + this.onRowsCreated, + this.onRowsUpdated, + this.onRowsDeleted, + }); +} + +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information +class DatabaseViewCache { + final String viewId; + late RowCache _rowCache; + final DatabaseViewListener _databaseViewListener; + final List _callbacks = []; + + UnmodifiableListView get rowInfos => _rowCache.rowInfos; + RowCache get rowCache => _rowCache; + + RowInfo? getRow(RowId rowId) => _rowCache.getRow(rowId); + + DatabaseViewCache({ + required this.viewId, + required FieldController fieldController, + }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) { + final delegate = RowDelegatesImpl(fieldController); + _rowCache = RowCache( + viewId: viewId, + fieldsDelegate: delegate, + cacheDelegate: delegate, + ); + + _databaseViewListener.start( + onRowsChanged: (result) { + result.fold( + (changeset) { + // Update the cache + _rowCache.applyRowsChanged(changeset); + + if (changeset.deletedRows.isNotEmpty) { + for (final callback in _callbacks) { + callback.onRowsDeleted?.call(changeset.deletedRows); + } + } + + if (changeset.updatedRows.isNotEmpty) { + for (final callback in _callbacks) { + callback.onRowsUpdated?.call( + changeset.updatedRows.map((e) => e.rowId).toList(), + _rowCache.changeReason, + ); + } + } + + if (changeset.insertedRows.isNotEmpty) { + for (final callback in _callbacks) { + callback.onRowsCreated?.call( + changeset.insertedRows + .map((insertedRow) => insertedRow.rowMeta.id) + .toList(), + ); + } + } + }, + (err) => Log.error(err), + ); + }, + onRowsVisibilityChanged: (result) { + result.fold( + (changeset) => _rowCache.applyRowsVisibility(changeset), + (err) => Log.error(err), + ); + }, + onReorderAllRows: (result) { + result.fold( + (rowIds) => _rowCache.reorderAllRows(rowIds), + (err) => Log.error(err), + ); + }, + onReorderSingleRow: (result) { + result.fold( + (reorderRow) => _rowCache.reorderSingleRow(reorderRow), + (err) => Log.error(err), + ); + }, + ); + + _rowCache.onRowsChanged( + (reason) { + for (final callback in _callbacks) { + callback.onNumOfRowsChanged?.call( + rowInfos, + _rowCache.rowByRowId, + reason, + ); + } + }, + ); + } + + Future dispose() async { + await _databaseViewListener.stop(); + await _rowCache.dispose(); + _callbacks.clear(); + } + + void addListener(DatabaseViewCallbacks callbacks) { + _callbacks.add(callbacks); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart new file mode 100644 index 0000000000..155b87097f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart'; + +typedef RowsVisibilityNotifierValue + = Either; + +typedef NumberOfRowsNotifierValue = Either; +typedef ReorderAllRowsNotifierValue = Either, FlowyError>; +typedef SingleRowNotifierValue = Either; + +class DatabaseViewListener { + final String viewId; + PublishNotifier? _rowsNotifier = PublishNotifier(); + PublishNotifier? _reorderAllRows = + PublishNotifier(); + PublishNotifier? _reorderSingleRow = + PublishNotifier(); + PublishNotifier? _rowsVisibility = + PublishNotifier(); + + DatabaseNotificationListener? _listener; + DatabaseViewListener({required this.viewId}); + + void start({ + required void Function(NumberOfRowsNotifierValue) onRowsChanged, + required void Function(ReorderAllRowsNotifierValue) onReorderAllRows, + required void Function(SingleRowNotifierValue) onReorderSingleRow, + required void Function(RowsVisibilityNotifierValue) onRowsVisibilityChanged, + }) { + if (_listener != null) { + _listener?.stop(); + } + + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + + _rowsNotifier?.addPublishListener(onRowsChanged); + _rowsVisibility?.addPublishListener(onRowsVisibilityChanged); + _reorderAllRows?.addPublishListener(onReorderAllRows); + _reorderSingleRow?.addPublishListener(onReorderSingleRow); + } + + void _handler(DatabaseNotification ty, Either result) { + switch (ty) { + case DatabaseNotification.DidUpdateViewRowsVisibility: + result.fold( + (payload) => _rowsVisibility?.value = + left(RowsVisibilityChangePB.fromBuffer(payload)), + (error) => _rowsVisibility?.value = right(error), + ); + break; + case DatabaseNotification.DidUpdateViewRows: + result.fold( + (payload) => + _rowsNotifier?.value = left(RowsChangePB.fromBuffer(payload)), + (error) => _rowsNotifier?.value = right(error), + ); + break; + case DatabaseNotification.DidReorderRows: + result.fold( + (payload) => _reorderAllRows?.value = + left(ReorderAllRowsPB.fromBuffer(payload).rowOrders), + (error) => _reorderAllRows?.value = right(error), + ); + break; + case DatabaseNotification.DidReorderSingleRow: + result.fold( + (payload) => _reorderSingleRow?.value = + left(ReorderSingleRowPB.fromBuffer(payload)), + (error) => _reorderSingleRow?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _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_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart new file mode 100644 index 0000000000..ba427d4a39 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -0,0 +1,500 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_board/appflowy_board.dart'; +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../application/field/field_controller.dart'; +import '../../application/row/row_cache.dart'; +import '../../application/database_controller.dart'; +import 'group_controller.dart'; + +part 'board_bloc.freezed.dart'; + +class BoardBloc extends Bloc { + final DatabaseController databaseController; + late final AppFlowyBoardController boardController; + final LinkedHashMap groupControllers = + LinkedHashMap(); + + FieldController get fieldController => databaseController.fieldController; + String get viewId => databaseController.viewId; + + BoardBloc({ + required ViewPB view, + required this.databaseController, + }) : super(BoardState.initial(view.id)) { + boardController = AppFlowyBoardController( + onMoveGroup: ( + fromGroupId, + fromIndex, + toGroupId, + toIndex, + ) { + databaseController.moveGroup( + fromGroupId: fromGroupId, + toGroupId: toGroupId, + ); + }, + onMoveGroupItem: ( + groupId, + fromIndex, + toIndex, + ) { + final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex); + final toRow = groupControllers[groupId]?.rowAtIndex(toIndex); + if (fromRow != null) { + databaseController.moveGroupRow( + fromRow: fromRow, + toRow: toRow, + groupId: groupId, + ); + } + }, + onMoveGroupItemToGroup: ( + fromGroupId, + fromIndex, + toGroupId, + toIndex, + ) { + final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex); + final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex); + if (fromRow != null) { + databaseController.moveGroupRow( + fromRow: fromRow, + toRow: toRow, + groupId: toGroupId, + ); + } + }, + ); + + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + await _openGrid(emit); + }, + createBottomRow: (groupId) async { + final startRowId = groupControllers[groupId]?.lastRow()?.id; + final result = await databaseController.createRow( + groupId: groupId, + startRowId: startRowId, + ); + result.fold( + (_) {}, + (err) => Log.error(err), + ); + }, + createHeaderRow: (String groupId) async { + final result = await databaseController.createRow(groupId: groupId); + result.fold( + (_) {}, + (err) => Log.error(err), + ); + }, + didCreateRow: (group, row, int? index) { + emit( + state.copyWith( + editingRow: Some( + BoardEditingRow( + group: group, + row: row, + index: index, + ), + ), + ), + ); + _groupItemStartEditing(group, row, true); + }, + startEditingRow: (group, row) { + emit( + state.copyWith( + editingRow: Some( + BoardEditingRow( + group: group, + row: row, + index: null, + ), + ), + ), + ); + _groupItemStartEditing(group, row, true); + }, + endEditingRow: (rowId) { + state.editingRow.fold(() => null, (editingRow) { + assert(editingRow.row.id == rowId); + _groupItemStartEditing(editingRow.group, editingRow.row, false); + emit(state.copyWith(editingRow: none())); + }); + }, + didReceiveGridUpdate: (DatabasePB grid) { + emit(state.copyWith(grid: Some(grid))); + }, + didReceiveError: (FlowyError error) { + emit(state.copyWith(noneOrError: some(error))); + }, + didReceiveGroups: (List groups) { + emit( + state.copyWith( + groupIds: groups.map((group) => group.groupId).toList(), + ), + ); + }, + ); + }, + ); + } + + void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) { + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); + return; + } + + boardController.enableGroupDragging(!isEdit); + } + + @override + Future close() async { + for (final controller in groupControllers.values) { + controller.dispose(); + } + return super.close(); + } + + void initializeGroups(List groups) { + for (final controller in groupControllers.values) { + controller.dispose(); + } + groupControllers.clear(); + boardController.clear(); + + boardController.addGroups( + groups + .where((group) => fieldController.getField(group.fieldId) != null) + .map((group) => initializeGroupData(group)) + .toList(), + ); + + for (final group in groups) { + final controller = initializeGroupController(group); + groupControllers[controller.group.groupId] = (controller); + } + } + + RowCache? getRowCache() { + return databaseController.rowCache; + } + + void _startListening() { + final onDatabaseChanged = DatabaseCallbacks( + onDatabaseChanged: (database) { + if (!isClosed) { + add(BoardEvent.didReceiveGridUpdate(database)); + } + }, + ); + final onGroupChanged = GroupCallbacks( + onGroupByField: (groups) { + if (isClosed) return; + initializeGroups(groups); + add(BoardEvent.didReceiveGroups(groups)); + }, + onDeleteGroup: (groupIds) { + if (isClosed) return; + boardController.removeGroups(groupIds); + }, + onInsertGroup: (insertGroups) { + if (isClosed) return; + final group = insertGroups.group; + final newGroup = initializeGroupData(group); + final controller = initializeGroupController(group); + groupControllers[controller.group.groupId] = (controller); + boardController.addGroup(newGroup); + }, + onUpdateGroup: (updatedGroups) { + if (isClosed) return; + for (final group in updatedGroups) { + final columnController = + boardController.getGroupController(group.groupId); + columnController?.updateGroupName(group.groupName); + } + }, + ); + + databaseController.addListener( + onDatabaseChanged: onDatabaseChanged, + onGroupChanged: onGroupChanged, + ); + } + + List _buildGroupItems(GroupPB group) { + final items = group.rows.map((row) { + final fieldInfo = fieldController.getField(group.fieldId); + return GroupItem( + row: row, + fieldInfo: fieldInfo!, + ); + }).toList(); + + return [...items]; + } + + Future _openGrid(Emitter emit) async { + final result = await databaseController.open(); + result.fold( + (grid) => emit( + state.copyWith(loadingState: GridLoadingState.finish(left(unit))), + ), + (err) => emit( + state.copyWith(loadingState: GridLoadingState.finish(right(err))), + ), + ); + } + + GroupController initializeGroupController(GroupPB group) { + final delegate = GroupControllerDelegateImpl( + controller: boardController, + fieldController: fieldController, + onNewColumnItem: (groupId, row, index) { + add(BoardEvent.didCreateRow(group, row, index)); + }, + ); + final controller = GroupController( + viewId: state.viewId, + group: group, + delegate: delegate, + ); + controller.startListening(); + return controller; + } + + AppFlowyGroupData initializeGroupData(GroupPB group) { + return AppFlowyGroupData( + id: group.groupId, + name: group.groupName, + items: _buildGroupItems(group), + customData: GroupData( + group: group, + fieldInfo: fieldController.getField(group.fieldId)!, + ), + ); + } +} + +@freezed +class BoardEvent with _$BoardEvent { + const factory BoardEvent.initial() = _InitialBoard; + const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow; + const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow; + const factory BoardEvent.didCreateRow( + GroupPB group, + RowMetaPB row, + int? index, + ) = _DidCreateRow; + const factory BoardEvent.startEditingRow( + GroupPB group, + RowMetaPB row, + ) = _StartEditRow; + const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow; + const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; + const factory BoardEvent.didReceiveGridUpdate( + DatabasePB grid, + ) = _DidReceiveGridUpdate; + const factory BoardEvent.didReceiveGroups(List groups) = + _DidReceiveGroups; +} + +@freezed +class BoardState with _$BoardState { + const factory BoardState({ + required String viewId, + required Option grid, + required List groupIds, + required Option editingRow, + required GridLoadingState loadingState, + required Option noneOrError, + }) = _BoardState; + + factory BoardState.initial(String viewId) => BoardState( + grid: none(), + viewId: viewId, + groupIds: [], + editingRow: none(), + noneOrError: none(), + loadingState: const _Loading(), + ); +} + +@freezed +class GridLoadingState with _$GridLoadingState { + const factory GridLoadingState.loading() = _Loading; + const factory GridLoadingState.finish( + Either successOrFail, + ) = _Finish; +} + +class GridFieldEquatable extends Equatable { + final UnmodifiableListView _fields; + const GridFieldEquatable( + UnmodifiableListView fields, + ) : _fields = fields; + + @override + List get props { + if (_fields.isEmpty) { + return []; + } + + return [ + _fields.length, + _fields + .map((field) => field.width) + .reduce((value, element) => value + element), + ]; + } + + UnmodifiableListView get value => UnmodifiableListView(_fields); +} + +class GroupItem extends AppFlowyGroupItem { + final RowMetaPB row; + final FieldInfo fieldInfo; + + GroupItem({ + required this.row, + required this.fieldInfo, + bool draggable = true, + }) { + super.draggable = draggable; + } + + @override + String get id => row.id.toString(); +} + +class GroupControllerDelegateImpl extends GroupControllerDelegate { + final FieldController fieldController; + final AppFlowyBoardController controller; + final void Function(String, RowMetaPB, int?) onNewColumnItem; + + GroupControllerDelegateImpl({ + required this.controller, + required this.fieldController, + required this.onNewColumnItem, + }); + + @override + void insertRow(GroupPB group, RowMetaPB row, int? index) { + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); + return; + } + + if (index != null) { + final item = GroupItem( + row: row, + fieldInfo: fieldInfo, + ); + controller.insertGroupItem(group.groupId, index, item); + } else { + final item = GroupItem( + row: row, + fieldInfo: fieldInfo, + ); + controller.addGroupItem(group.groupId, item); + } + } + + @override + void removeRow(GroupPB group, RowId rowId) { + controller.removeGroupItem(group.groupId, rowId.toString()); + } + + @override + void updateRow(GroupPB group, RowMetaPB row) { + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); + return; + } + controller.updateGroupItem( + group.groupId, + GroupItem( + row: row, + fieldInfo: fieldInfo, + ), + ); + } + + @override + void addNewRow(GroupPB group, RowMetaPB row, int? index) { + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); + return; + } + final item = GroupItem( + row: row, + fieldInfo: fieldInfo, + draggable: false, + ); + + if (index != null) { + controller.insertGroupItem(group.groupId, index, item); + } else { + controller.addGroupItem(group.groupId, item); + } + onNewColumnItem(group.groupId, row, index); + } +} + +class BoardEditingRow { + GroupPB group; + RowMetaPB row; + int? index; + + BoardEditingRow({ + required this.group, + required this.row, + required this.index, + }); +} + +class GroupData { + final GroupPB group; + final FieldInfo fieldInfo; + GroupData({ + required this.group, + required this.fieldInfo, + }); + + CheckboxGroup? asCheckboxGroup() { + if (fieldType != FieldType.Checkbox) return null; + return CheckboxGroup(group); + } + + FieldType get fieldType => fieldInfo.fieldType; +} + +class CheckboxGroup { + final GroupPB group; + + CheckboxGroup(this.group); + +// Hardcode value: "Yes" that equal to the value defined in Rust +// pub const CHECK: &str = "Yes"; + bool get isCheck => group.groupId == "Yes"; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart new file mode 100644 index 0000000000..feefa34db7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart @@ -0,0 +1,12 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; + +class BoardGroupService { + final String viewId; + FieldPB? groupField; + + BoardGroupService(this.viewId); + + void setGroupField(FieldPB field) { + groupField = field; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart new file mode 100644 index 0000000000..0e4a315de3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart @@ -0,0 +1,132 @@ +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:dartz/dartz.dart'; + +typedef OnGroupError = void Function(FlowyError); + +abstract class GroupControllerDelegate { + void removeRow(GroupPB group, RowId rowId); + void insertRow(GroupPB group, RowMetaPB row, int? index); + void updateRow(GroupPB group, RowMetaPB row); + void addNewRow(GroupPB group, RowMetaPB row, int? index); +} + +class GroupController { + final GroupPB group; + final SingleGroupListener _listener; + final GroupControllerDelegate delegate; + + GroupController({ + required String viewId, + required this.group, + required this.delegate, + }) : _listener = SingleGroupListener(group); + + RowMetaPB? rowAtIndex(int index) { + if (index < group.rows.length) { + return group.rows[index]; + } else { + return null; + } + } + + RowMetaPB? lastRow() { + if (group.rows.isEmpty) return null; + return group.rows.last; + } + + void startListening() { + _listener.start( + onGroupChanged: (result) { + result.fold( + (GroupRowsNotificationPB changeset) { + for (final deletedRow in changeset.deletedRows) { + group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); + delegate.removeRow(group, deletedRow); + } + + for (final insertedRow in changeset.insertedRows) { + final index = insertedRow.hasIndex() ? insertedRow.index : null; + if (insertedRow.hasIndex() && + group.rows.length > insertedRow.index) { + group.rows.insert(insertedRow.index, insertedRow.rowMeta); + } else { + group.rows.add(insertedRow.rowMeta); + } + + if (insertedRow.isNew) { + delegate.addNewRow(group, insertedRow.rowMeta, index); + } else { + delegate.insertRow(group, insertedRow.rowMeta, index); + } + } + + for (final updatedRow in changeset.updatedRows) { + final index = group.rows.indexWhere( + (rowPB) => rowPB.id == updatedRow.id, + ); + + if (index != -1) { + group.rows[index] = updatedRow; + delegate.updateRow(group, updatedRow); + } + } + }, + (err) => Log.error(err), + ); + }, + ); + } + + Future dispose() async { + _listener.stop(); + } +} + +typedef UpdateGroupNotifiedValue = Either; + +class SingleGroupListener { + final GroupPB group; + PublishNotifier? _groupNotifier = PublishNotifier(); + DatabaseNotificationListener? _listener; + SingleGroupListener(this.group); + + void start({ + required void Function(UpdateGroupNotifiedValue) onGroupChanged, + }) { + _groupNotifier?.addPublishListener(onGroupChanged); + _listener = DatabaseNotificationListener( + objectId: group.groupId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateGroupRow: + result.fold( + (payload) => _groupNotifier?.value = + left(GroupRowsNotificationPB.fromBuffer(payload)), + (error) => _groupNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _groupNotifier?.dispose(); + _groupNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/toolbar/board_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/toolbar/board_setting_bloc.dart new file mode 100644 index 0000000000..4eb4440f11 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/toolbar/board_setting_bloc.dart @@ -0,0 +1,43 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +part 'board_setting_bloc.freezed.dart'; + +class BoardSettingBloc extends Bloc { + final String viewId; + BoardSettingBloc({required this.viewId}) + : super(BoardSettingState.initial()) { + on( + (event, emit) async { + event.when( + performAction: (action) { + emit(state.copyWith(selectedAction: Some(action))); + }, + ); + }, + ); + } +} + +@freezed +class BoardSettingEvent with _$BoardSettingEvent { + const factory BoardSettingEvent.performAction(BoardSettingAction action) = + _PerformAction; +} + +@freezed +class BoardSettingState with _$BoardSettingState { + const factory BoardSettingState({ + required Option selectedAction, + }) = _BoardSettingState; + + factory BoardSettingState.initial() => BoardSettingState( + selectedAction: none(), + ); +} + +enum BoardSettingAction { + properties, + groups, +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart new file mode 100644 index 0000000000..22e5858d4f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart @@ -0,0 +1,33 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class BoardPluginBuilder implements PluginBuilder { + @override + Plugin build(dynamic data) { + if (data is ViewPB) { + return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); + } else { + throw FlowyPluginException.invalidData; + } + } + + @override + String get menuName => LocaleKeys.board_menuName.tr(); + + @override + String get menuIcon => "editor/board"; + + @override + PluginType get pluginType => PluginType.board; + + @override + ViewLayoutPB? get layoutType => ViewLayoutPB.Board; +} + +class BoardPluginConfig implements PluginConfig { + @override + bool get creatable => true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart new file mode 100644 index 0000000000..d3482b7675 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -0,0 +1,410 @@ +// ignore_for_file: unused_field + +import 'dart:collection'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_board/appflowy_board.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart' hide Card; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../widgets/card/cells/card_cell.dart'; +import '../../widgets/card/card_cell_builder.dart'; +import '../../widgets/row/cell_builder.dart'; +import '../application/board_bloc.dart'; +import '../../widgets/card/card.dart'; +import 'toolbar/board_setting_bar.dart'; + +class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { + @override + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ) { + return BoardPage( + key: _makeValueKey(controller), + view: view, + databaseController: controller, + ); + } + + @override + Widget settingBar(BuildContext context, DatabaseController controller) { + return BoardSettingBar( + key: _makeValueKey(controller), + databaseController: controller, + ); + } + + @override + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ) { + return SizedBox.fromSize(); + } + + ValueKey _makeValueKey(DatabaseController controller) { + return ValueKey(controller.viewId); + } +} + +class BoardPage extends StatelessWidget { + final DatabaseController databaseController; + BoardPage({ + required this.view, + required this.databaseController, + Key? key, + this.onEditStateChanged, + }) : super(key: ValueKey(view.id)); + + final ViewPB view; + + /// Called when edit state changed + final VoidCallback? onEditStateChanged; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => BoardBloc( + view: view, + databaseController: databaseController, + )..add(const BoardEvent.initial()), + child: BlocBuilder( + buildWhen: (p, c) => p.loadingState != c.loadingState, + builder: (context, state) { + return state.loadingState.map( + loading: (_) => + const Center(child: CircularProgressIndicator.adaptive()), + finish: (result) { + return result.successOrFail.fold( + (_) => BoardContent( + onEditStateChanged: onEditStateChanged, + ), + (err) => FlowyErrorPage.message(err.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),), + ); + }, + ); + }, + ), + ); + } +} + +class BoardContent extends StatefulWidget { + const BoardContent({ + Key? key, + this.onEditStateChanged, + }) : super(key: key); + + final VoidCallback? onEditStateChanged; + + @override + State createState() => _BoardContentState(); +} + +class _BoardContentState extends State { + late AppFlowyBoardScrollController scrollManager; + final renderHook = RowCardRenderHook(); + + final config = const AppFlowyBoardConfig( + groupBackgroundColor: Color(0xffF7F8FC), + ); + + @override + void initState() { + scrollManager = AppFlowyBoardScrollController(); + renderHook.addSelectOptionHook((options, groupId, _) { + // The cell should hide if the option id is equal to the groupId. + final isInGroup = + options.where((element) => element.id == groupId).isNotEmpty; + if (isInGroup || options.isEmpty) { + return const SizedBox(); + } + return null; + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + _handleEditStateChanged(state, context); + widget.onEditStateChanged?.call(); + }, + child: BlocBuilder( + buildWhen: (previous, current) => previous.groupIds != current.groupIds, + builder: (context, state) { + return Padding( + padding: GridSize.contentInsets, + child: _buildBoard(context), + ); + }, + ), + ); + } + + Widget _buildBoard(BuildContext context) { + return AppFlowyBoard( + boardScrollController: scrollManager, + scrollController: ScrollController(), + controller: context.read().boardController, + headerBuilder: _buildHeader, + footerBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), + groupConstraints: const BoxConstraints.tightFor(width: 300), + config: AppFlowyBoardConfig( + groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant, + ), + ); + } + + void _handleEditStateChanged(BoardState state, BuildContext context) { + state.editingRow.fold( + () => null, + (editingRow) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (editingRow.index != null) { + } else { + scrollManager.scrollToBottom(editingRow.group.groupId); + } + }); + }, + ); + } + + @override + void dispose() { + super.dispose(); + } + + Widget _buildHeader( + BuildContext context, + AppFlowyGroupData groupData, + ) { + final boardCustomData = groupData.customData as GroupData; + return AppFlowyGroupHeader( + title: Flexible( + fit: FlexFit.tight, + child: FlowyText.medium( + groupData.headerData.groupName, + fontSize: 14, + overflow: TextOverflow.clip, + ), + ), + icon: _buildHeaderIcon(boardCustomData), + addIcon: SizedBox( + height: 20, + width: 20, + child: svgWidget( + "home/add", + color: Theme.of(context).iconTheme.color, + ), + ), + onAddButtonClick: () { + context.read().add( + BoardEvent.createHeaderRow(groupData.id), + ); + }, + height: 50, + margin: config.headerPadding, + ); + } + + Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) { + // final boardCustomData = columnData.customData as BoardCustomData; + // final group = boardCustomData.group; + + return AppFlowyGroupFooter( + icon: SizedBox( + height: 20, + width: 20, + child: svgWidget( + "home/add", + color: Theme.of(context).iconTheme.color, + ), + ), + title: FlowyText.medium( + LocaleKeys.board_column_create_new_card.tr(), + fontSize: 14, + ), + height: 50, + margin: config.footerPadding, + onAddButtonClick: () { + context.read().add( + BoardEvent.createBottomRow(columnData.id), + ); + }, + ); + } + + Widget _buildCard( + BuildContext context, + AppFlowyGroupData afGroupData, + AppFlowyGroupItem afGroupItem, + ) { + final groupItem = afGroupItem as GroupItem; + final groupData = afGroupData.customData as GroupData; + final rowMeta = groupItem.row; + final rowCache = context.read().getRowCache(); + + /// Return placeholder widget if the rowCache is null. + if (rowCache == null) return SizedBox(key: ObjectKey(groupItem)); + final cellCache = rowCache.cellCache; + final fieldController = context.read().fieldController; + final viewId = context.read().viewId; + + final cellBuilder = CardCellBuilder(cellCache); + bool isEditing = false; + context.read().state.editingRow.fold( + () => null, + (editingRow) { + isEditing = editingRow.row.id == groupItem.row.id; + }, + ); + + final groupItemId = groupItem.row.id + groupData.group.groupId; + return AppFlowyGroupCard( + key: ValueKey(groupItemId), + margin: config.cardPadding, + decoration: _makeBoxDecoration(context), + child: RowCard( + rowMeta: rowMeta, + viewId: viewId, + rowCache: rowCache, + cardData: groupData.group.groupId, + groupingFieldId: groupItem.fieldInfo.id, + isEditing: isEditing, + cellBuilder: cellBuilder, + renderHook: renderHook, + openCard: (context) => _openCard( + viewId, + groupData.group.groupId, + fieldController, + rowMeta, + rowCache, + context, + ), + onStartEditing: () { + context.read().add( + BoardEvent.startEditingRow( + groupData.group, + groupItem.row, + ), + ); + }, + onEndEditing: () { + context + .read() + .add(BoardEvent.endEditingRow(groupItem.row.id)); + }, + ), + ); + } + + BoxDecoration _makeBoxDecoration(BuildContext context) { + final borderSide = BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0, + ); + final isLightMode = Theme.of(context).brightness == Brightness.light; + return BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: isLightMode ? Border.fromBorderSide(borderSide) : null, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ); + } + + void _openCard( + String viewId, + String groupId, + FieldController fieldController, + RowMetaPB rowMetaPB, + RowCache rowCache, + BuildContext context, + ) { + final rowInfo = RowInfo( + viewId: viewId, + fields: UnmodifiableListView(fieldController.fieldInfos), + rowMeta: rowMetaPB, + rowId: rowMetaPB.id, + ); + + final dataController = RowController( + rowMeta: rowInfo.rowMeta, + viewId: rowInfo.viewId, + rowCache: rowCache, + groupId: groupId, + ); + + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + rowController: dataController, + ); + }, + ); + } +} + +Widget? _buildHeaderIcon(GroupData customData) { + Widget? widget; + switch (customData.fieldType) { + case FieldType.Checkbox: + final group = customData.asCheckboxGroup()!; + if (group.isCheck) { + widget = svgWidget('editor/editor_check'); + } else { + widget = svgWidget('editor/editor_uncheck'); + } + break; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + break; + case FieldType.MultiSelect: + break; + case FieldType.Number: + break; + case FieldType.RichText: + break; + case FieldType.SingleSelect: + break; + case FieldType.URL: + break; + case FieldType.Checklist: + break; + } + + if (widget != null) { + widget = SizedBox( + width: 20, + height: 20, + child: widget, + ); + } + return widget; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart new file mode 100644 index 0000000000..f3c357749d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart @@ -0,0 +1,24 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:flutter/material.dart'; + +class BoardSettingBar extends StatelessWidget { + final DatabaseController databaseController; + const BoardSettingBar({ + required this.databaseController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: Row( + children: [ + const Spacer(), + SettingButton(databaseController: databaseController), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/tests/integrate_test/card_test.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/tests/integrate_test/card_test.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/board/tests/integrate_test/card_test.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/board/tests/integrate_test/card_test.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart new file mode 100644 index 0000000000..cf9bba21c3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -0,0 +1,469 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:dartz/dartz.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../application/database_controller.dart'; +import '../../application/row/row_cache.dart'; + +part 'calendar_bloc.freezed.dart'; + +class CalendarBloc extends Bloc { + final DatabaseController databaseController; + Map fieldInfoByFieldId = {}; + + // Getters + String get viewId => databaseController.viewId; + FieldController get fieldController => databaseController.fieldController; + CellCache get cellCache => databaseController.rowCache.cellCache; + RowCache get rowCache => databaseController.rowCache; + + CalendarBloc({required ViewPB view, required this.databaseController}) + : super(CalendarState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + await _openDatabase(emit); + _loadAllEvents(); + }, + didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) { + // If the field id changed, reload all events + state.settings.fold(() => null, (oldSetting) { + if (oldSetting.fieldId != settings.fieldId) { + _loadAllEvents(); + } + }); + emit(state.copyWith(settings: Some(settings))); + }, + didReceiveDatabaseUpdate: (DatabasePB database) { + emit(state.copyWith(database: Some(database))); + }, + didLoadAllEvents: (events) { + final calenderEvents = _calendarEventDataFromEventPBs(events); + emit( + state.copyWith( + initialEvents: calenderEvents, + allEvents: calenderEvents, + ), + ); + }, + createEvent: (DateTime date, String title) async { + await _createEvent(date, title); + }, + moveEvent: (CalendarDayEvent event, DateTime date) async { + await _moveEvent(event, date); + }, + didCreateEvent: (CalendarEventData event) { + emit(state.copyWith(editingEvent: event)); + }, + updateCalendarLayoutSetting: + (CalendarLayoutSettingPB layoutSetting) async { + await _updateCalendarLayoutSetting(layoutSetting); + }, + didUpdateEvent: (CalendarEventData eventData) { + final allEvents = [...state.allEvents]; + final index = allEvents.indexWhere( + (element) => element.event!.eventId == eventData.event!.eventId, + ); + if (index != -1) { + allEvents[index] = eventData; + } + emit(state.copyWith(allEvents: allEvents, updateEvent: eventData)); + }, + didDeleteEvents: (List deletedRowIds) { + final events = [...state.allEvents]; + events.retainWhere( + (element) => !deletedRowIds.contains(element.event!.eventId), + ); + emit( + state.copyWith( + allEvents: events, + deleteEventIds: deletedRowIds, + ), + ); + }, + didReceiveEvent: (CalendarEventData event) { + emit( + state.copyWith( + allEvents: [...state.allEvents, event], + newEvent: event, + ), + ); + }, + ); + }, + ); + } + + FieldInfo? _getCalendarFieldInfo(String fieldId) { + final fieldInfos = databaseController.fieldController.fieldInfos; + final index = fieldInfos.indexWhere( + (element) => element.field.id == fieldId, + ); + if (index != -1) { + return fieldInfos[index]; + } else { + return null; + } + } + + FieldInfo? _getTitleFieldInfo() { + final fieldInfos = databaseController.fieldController.fieldInfos; + final index = fieldInfos.indexWhere( + (element) => element.field.isPrimary, + ); + if (index != -1) { + return fieldInfos[index]; + } else { + return null; + } + } + + Future _openDatabase(Emitter emit) async { + final result = await databaseController.open(); + result.fold( + (database) => emit( + state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))), + ), + (err) => emit( + state.copyWith(loadingState: DatabaseLoadingState.finish(right(err))), + ), + ); + } + + Future _createEvent(DateTime date, String title) async { + return state.settings.fold( + () { + Log.warn('Calendar settings not found'); + }, + (settings) async { + final dateField = _getCalendarFieldInfo(settings.fieldId); + final titleField = _getTitleFieldInfo(); + if (dateField != null && titleField != null) { + final newRow = await databaseController.createRow( + withCells: (builder) { + builder.insertDate(dateField, date); + builder.insertText(titleField, title); + }, + ).then( + (result) => result.fold( + (newRow) => newRow, + (err) { + Log.error(err); + return null; + }, + ), + ); + + if (newRow != null) { + final event = await _loadEvent(newRow.id); + if (event != null && !isClosed) { + add(CalendarEvent.didCreateEvent(event)); + } + } + } + }, + ); + } + + Future _moveEvent(CalendarDayEvent event, DateTime date) async { + final timestamp = _eventTimestamp(event, date); + final payload = MoveCalendarEventPB( + cellPath: CellIdPB( + viewId: viewId, + rowId: event.eventId, + fieldId: event.dateFieldId, + ), + timestamp: timestamp, + ); + return DatabaseEventMoveCalendarEvent(payload).send().then((result) { + return result.fold( + (_) async { + final modifiedEvent = await _loadEvent(event.eventId); + add(CalendarEvent.didUpdateEvent(modifiedEvent!)); + }, + (err) { + Log.error(err); + return null; + }, + ); + }); + } + + Future _updateCalendarLayoutSetting( + CalendarLayoutSettingPB layoutSetting, + ) async { + return databaseController.updateLayoutSetting(layoutSetting); + } + + Future?> _loadEvent(RowId rowId) async { + final payload = RowIdPB(viewId: viewId, rowId: rowId); + return DatabaseEventGetCalendarEvent(payload).send().then((result) { + return result.fold( + (eventPB) { + final calendarEvent = _calendarEventDataFromEventPB(eventPB); + return calendarEvent; + }, + (r) { + Log.error(r); + return null; + }, + ); + }); + } + + Future _loadAllEvents() async { + final payload = CalendarEventRequestPB.create()..viewId = viewId; + DatabaseEventGetAllCalendarEvents(payload).send().then((result) { + result.fold( + (events) { + if (!isClosed) { + add(CalendarEvent.didLoadAllEvents(events.items)); + } + }, + (r) => Log.error(r), + ); + }); + } + + List> _calendarEventDataFromEventPBs( + List eventPBs, + ) { + final calendarEvents = >[]; + for (final eventPB in eventPBs) { + final event = _calendarEventDataFromEventPB(eventPB); + if (event != null) { + calendarEvents.add(event); + } + } + return calendarEvents; + } + + CalendarEventData? _calendarEventDataFromEventPB( + CalendarEventPB eventPB, + ) { + final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId]; + if (fieldInfo == null) { + return null; + } + + // timestamp is stored as seconds, but constructor requires milliseconds + final date = DateTime.fromMillisecondsSinceEpoch( + eventPB.timestamp.toInt() * 1000, + ); + + final eventData = CalendarDayEvent( + event: eventPB, + eventId: eventPB.rowMeta.id, + dateFieldId: eventPB.dateFieldId, + date: date, + ); + + return CalendarEventData( + title: eventPB.title, + date: date, + event: eventData, + ); + } + + void _startListening() { + final onDatabaseChanged = DatabaseCallbacks( + onDatabaseChanged: (database) { + if (isClosed) return; + }, + onFieldsChanged: (fieldInfos) { + if (isClosed) { + return; + } + fieldInfoByFieldId = { + for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo + }; + }, + onRowsCreated: (rowIds) async { + if (isClosed) { + return; + } + for (final id in rowIds) { + final event = await _loadEvent(id); + if (event != null && !isClosed) { + add(CalendarEvent.didReceiveEvent(event)); + } + } + }, + onRowsDeleted: (rowIds) { + if (isClosed) { + return; + } + add(CalendarEvent.didDeleteEvents(rowIds)); + }, + onRowsUpdated: (rowIds, reason) async { + if (isClosed) { + return; + } + for (final id in rowIds) { + final event = await _loadEvent(id); + if (event != null) { + if (isEventDayChanged(event)) { + add(CalendarEvent.didDeleteEvents([id])); + add(CalendarEvent.didReceiveEvent(event)); + } else { + add(CalendarEvent.didUpdateEvent(event)); + } + } + } + }, + ); + + final onLayoutChanged = DatabaseLayoutSettingCallbacks( + onLayoutChanged: _didReceiveLayoutSetting, + onLoadLayout: _didReceiveLayoutSetting, + ); + + databaseController.addListener( + onDatabaseChanged: onDatabaseChanged, + onLayoutChanged: onLayoutChanged, + ); + } + + void _didReceiveLayoutSetting(DatabaseLayoutSettingPB layoutSetting) { + if (layoutSetting.hasCalendar()) { + if (isClosed) { + return; + } + add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar)); + } + } + + bool isEventDayChanged(CalendarEventData event) { + final index = state.allEvents.indexWhere( + (element) => element.event!.eventId == event.event!.eventId, + ); + if (index == -1) { + return false; + } + return state.allEvents[index].date.day != event.date.day; + } + + Int64 _eventTimestamp(CalendarDayEvent event, DateTime date) { + final time = + event.date.hour * 3600 + event.date.minute * 60 + event.date.second; + return Int64(date.millisecondsSinceEpoch ~/ 1000 + time); + } +} + +typedef Events = List>; + +@freezed +class CalendarEvent with _$CalendarEvent { + const factory CalendarEvent.initial() = _InitialCalendar; + + // Called after loading the calendar layout setting from the backend + const factory CalendarEvent.didReceiveCalendarSettings( + CalendarLayoutSettingPB settings, + ) = _ReceiveCalendarSettings; + + // Called after loading all the current evnets + const factory CalendarEvent.didLoadAllEvents(List events) = + _ReceiveCalendarEvents; + + // Called when specific event was updated + const factory CalendarEvent.didUpdateEvent( + CalendarEventData event, + ) = _DidUpdateEvent; + + // Called after creating a new event + const factory CalendarEvent.didCreateEvent( + CalendarEventData event, + ) = _DidReceiveNewEvent; + + // Called when receive a new event + const factory CalendarEvent.didReceiveEvent( + CalendarEventData event, + ) = _DidReceiveEvent; + + // Called when deleting events + const factory CalendarEvent.didDeleteEvents(List rowIds) = + _DidDeleteEvents; + + // Called when creating a new event + const factory CalendarEvent.createEvent(DateTime date, String title) = + _CreateEvent; + + // Called when moving an event + const factory CalendarEvent.moveEvent(CalendarDayEvent event, DateTime date) = + _MoveEvent; + + // Called when updating the calendar's layout settings + const factory CalendarEvent.updateCalendarLayoutSetting( + CalendarLayoutSettingPB layoutSetting, + ) = _UpdateCalendarLayoutSetting; + + const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = + _ReceiveDatabaseUpdate; +} + +@freezed +class CalendarState with _$CalendarState { + const factory CalendarState({ + required Option database, + // events by row id + required Events allEvents, + required Events initialEvents, + CalendarEventData? editingEvent, + CalendarEventData? newEvent, + CalendarEventData? updateEvent, + required List deleteEventIds, + required Option settings, + required DatabaseLoadingState loadingState, + required Option noneOrError, + }) = _CalendarState; + + factory CalendarState.initial() => CalendarState( + database: none(), + allEvents: [], + initialEvents: [], + deleteEventIds: [], + settings: none(), + noneOrError: none(), + loadingState: const _Loading(), + ); +} + +@freezed +class DatabaseLoadingState with _$DatabaseLoadingState { + const factory DatabaseLoadingState.loading() = _Loading; + const factory DatabaseLoadingState.finish( + Either successOrFail, + ) = _Finish; +} + +class CalendarEditingRow { + RowPB row; + int? index; + + CalendarEditingRow({ + required this.row, + required this.index, + }); +} + +@freezed +class CalendarDayEvent with _$CalendarDayEvent { + const factory CalendarDayEvent({ + required CalendarEventPB event, + required String dateFieldId, + required String eventId, + required DateTime date, + }) = _CalendarDayEvent; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart new file mode 100644 index 0000000000..1834de9224 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart @@ -0,0 +1,90 @@ +import 'package:appflowy/plugins/database_view/application/layout/layout_setting_listener.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:bloc/bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'calendar_setting_bloc.freezed.dart'; + +typedef DayOfWeek = int; + +class CalendarSettingBloc + extends Bloc { + final String viewId; + final DatabaseLayoutSettingListener _listener; + + CalendarSettingBloc({ + required this.viewId, + required CalendarLayoutSettingPB? layoutSettings, + }) : _listener = DatabaseLayoutSettingListener(viewId), + super(CalendarSettingState.initial(layoutSettings)) { + on((event, emit) { + event.when( + init: () { + _startListening(); + }, + performAction: (action) { + emit(state.copyWith(selectedAction: Some(action))); + }, + updateLayoutSetting: (setting) { + emit(state.copyWith(layoutSetting: Some(setting))); + }, + ); + }); + } + + void _startListening() { + _listener.start( + onLayoutChanged: (result) { + if (isClosed) { + return; + } + + result.fold( + (setting) => + add(CalendarSettingEvent.updateLayoutSetting(setting.calendar)), + (r) => Log.error(r), + ); + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class CalendarSettingEvent with _$CalendarSettingEvent { + const factory CalendarSettingEvent.init() = _Init; + const factory CalendarSettingEvent.performAction( + CalendarSettingAction action, + ) = _PerformAction; + const factory CalendarSettingEvent.updateLayoutSetting( + CalendarLayoutSettingPB setting, + ) = _UpdateLayoutSetting; +} + +enum CalendarSettingAction { + properties, + layout, +} + +@freezed +class CalendarSettingState with _$CalendarSettingState { + const factory CalendarSettingState({ + required Option selectedAction, + required Option layoutSetting, + }) = _CalendarSettingState; + + factory CalendarSettingState.initial( + CalendarLayoutSettingPB? layoutSettings, + ) => + CalendarSettingState( + selectedAction: none(), + layoutSetting: layoutSettings == null ? none() : Some(layoutSettings), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart new file mode 100644 index 0000000000..761fdeb367 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart @@ -0,0 +1,167 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/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/protobuf.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../application/database_controller.dart'; +import '../../application/row/row_cache.dart'; + +part 'unschedule_event_bloc.freezed.dart'; + +class UnscheduleEventsBloc + extends Bloc { + final DatabaseController databaseController; + Map fieldInfoByFieldId = {}; + + // Getters + String get viewId => databaseController.viewId; + FieldController get fieldController => databaseController.fieldController; + CellCache get cellCache => databaseController.rowCache.cellCache; + RowCache get rowCache => databaseController.rowCache; + + UnscheduleEventsBloc({ + required this.databaseController, + }) : super(UnscheduleEventsState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + _loadAllEvents(); + }, + didLoadAllEvents: (events) { + emit( + state.copyWith( + allEvents: events, + unscheduleEvents: + events.where((element) => !element.isScheduled).toList(), + ), + ); + }, + didDeleteEvents: (List deletedRowIds) { + final events = [...state.allEvents]; + events.retainWhere( + (element) => !deletedRowIds.contains(element.rowMeta.id), + ); + emit( + state.copyWith( + allEvents: events, + unscheduleEvents: + events.where((element) => !element.isScheduled).toList(), + ), + ); + }, + didReceiveEvent: (CalendarEventPB event) { + emit( + state.copyWith( + allEvents: [...state.allEvents, event], + ), + ); + }, + ); + }, + ); + } + + Future _loadEvent( + RowId rowId, + ) async { + final payload = RowIdPB(viewId: viewId, rowId: rowId); + return DatabaseEventGetCalendarEvent(payload).send().then( + (result) => result.fold( + (eventPB) => eventPB, + (r) { + Log.error(r); + return null; + }, + ), + ); + } + + Future _loadAllEvents() async { + final payload = CalendarEventRequestPB.create()..viewId = viewId; + DatabaseEventGetAllCalendarEvents(payload).send().then((result) { + result.fold( + (events) { + if (!isClosed) { + add(UnscheduleEventsEvent.didLoadAllEvents(events.items)); + } + }, + (r) => Log.error(r), + ); + }); + } + + void _startListening() { + final onDatabaseChanged = DatabaseCallbacks( + onRowsCreated: (rowIds) async { + if (isClosed) { + return; + } + for (final id in rowIds) { + final event = await _loadEvent(id); + if (event != null && !isClosed) { + add(UnscheduleEventsEvent.didReceiveEvent(event)); + } + } + }, + onRowsDeleted: (rowIds) { + if (isClosed) { + return; + } + add(UnscheduleEventsEvent.didDeleteEvents(rowIds)); + }, + onRowsUpdated: (rowIds, reason) async { + if (isClosed) { + return; + } + for (final id in rowIds) { + final event = await _loadEvent(id); + if (event != null) { + add(UnscheduleEventsEvent.didDeleteEvents([id])); + add(UnscheduleEventsEvent.didReceiveEvent(event)); + } + } + }, + ); + + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); + } +} + +@freezed +class UnscheduleEventsEvent with _$UnscheduleEventsEvent { + const factory UnscheduleEventsEvent.initial() = _InitialCalendar; + + // Called after loading all the current evnets + const factory UnscheduleEventsEvent.didLoadAllEvents( + List events, + ) = _ReceiveUnscheduleEventsEvents; + + const factory UnscheduleEventsEvent.didDeleteEvents(List rowIds) = + _DidDeleteEvents; + + const factory UnscheduleEventsEvent.didReceiveEvent( + CalendarEventPB event, + ) = _DidReceiveEvent; +} + +@freezed +class UnscheduleEventsState with _$UnscheduleEventsState { + const factory UnscheduleEventsState({ + required Option database, + required List allEvents, + required List unscheduleEvents, + }) = _UnscheduleEventsState; + + factory UnscheduleEventsState.initial() => UnscheduleEventsState( + database: none(), + allEvents: [], + unscheduleEvents: [], + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart new file mode 100644 index 0000000000..aa5047ca2f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart @@ -0,0 +1,33 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; + +class CalendarPluginBuilder extends PluginBuilder { + @override + Plugin build(dynamic data) { + if (data is ViewPB) { + return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); + } else { + throw FlowyPluginException.invalidData; + } + } + + @override + String get menuName => LocaleKeys.calendar_menuName.tr(); + + @override + String get menuIcon => "editor/date"; + + @override + PluginType get pluginType => PluginType.calendar; + + @override + ViewLayoutPB? get layoutType => ViewLayoutPB.Calendar; +} + +class CalendarPluginConfig implements PluginConfig { + @override + bool get creatable => true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart new file mode 100644 index 0000000000..be0e2b575c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart @@ -0,0 +1,445 @@ +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import '../../grid/presentation/layout/sizes.dart'; +import '../../widgets/row/cells/select_option_cell/extension.dart'; +import '../application/calendar_bloc.dart'; +import 'calendar_page.dart'; + +class CalendarDayCard extends StatelessWidget { + final String viewId; + final bool isToday; + final bool isInMonth; + final DateTime date; + final RowCache _rowCache; + final List events; + final void Function(DateTime) onCreateEvent; + + const CalendarDayCard({ + required this.viewId, + required this.isToday, + required this.isInMonth, + required this.date, + required this.onCreateEvent, + required RowCache rowCache, + required this.events, + Key? key, + }) : _rowCache = rowCache, + super(key: key); + + @override + Widget build(BuildContext context) { + Color backgroundColor = Theme.of(context).colorScheme.surface; + if (!isInMonth) { + backgroundColor = AFThemeExtension.of(context).lightGreyHover; + } + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return ChangeNotifierProvider( + create: (_) => _CardEnterNotifier(), + builder: (context, child) { + final child = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _Header( + date: date, + isInMonth: isInMonth, + isToday: isToday, + ), + + // Add a separator between the header and the content. + VSpace(GridSize.typeOptionSeparatorHeight), + + // List of cards or empty space + if (events.isNotEmpty) + _EventList( + events: events, + viewId: viewId, + rowCache: _rowCache, + constraints: constraints, + ), + ], + ); + + return Stack( + children: [ + GestureDetector( + onDoubleTap: () => onCreateEvent(date), + child: Container(color: backgroundColor), + ), + DragTarget( + builder: (context, candidate, __) { + return Stack( + fit: StackFit.expand, + children: [ + if (candidate.isNotEmpty) + Container( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: child, + ) + ], + ); + }, + onWillAccept: (CalendarDayEvent? event) { + if (event == null) { + return false; + } + return !isSameDay(event.date, date); + }, + onAccept: (CalendarDayEvent event) { + context + .read() + .add(CalendarEvent.moveEvent(event, date)); + }, + ), + _NewEventButton(onCreate: () => onCreateEvent(date)), + MouseRegion( + onEnter: (p) => notifyEnter(context, true), + onExit: (p) => notifyEnter(context, false), + opaque: false, + hitTestBehavior: HitTestBehavior.translucent, + ), + ], + ); + }, + ); + }, + ); + } + + notifyEnter(BuildContext context, bool isEnter) { + Provider.of<_CardEnterNotifier>( + context, + listen: false, + ).onEnter = isEnter; + } +} + +class _Header extends StatelessWidget { + final bool isToday; + final bool isInMonth; + final DateTime date; + const _Header({ + required this.isToday, + required this.isInMonth, + required this.date, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _DayBadge( + isToday: isToday, + isInMonth: isInMonth, + date: date, + ), + ); + } +} + +class _NewEventButton extends StatelessWidget { + final VoidCallback onCreate; + const _NewEventButton({required this.onCreate, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer<_CardEnterNotifier>( + builder: (context, notifier, _) { + if (!notifier.onEnter) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyIconButton( + onPressed: onCreate, + iconPadding: EdgeInsets.zero, + icon: const FlowySvg(name: "home/add"), + fillColor: Theme.of(context).colorScheme.background, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + width: 22, + ), + ); + }, + ); + } +} + +class _DayBadge extends StatelessWidget { + final bool isToday; + final bool isInMonth; + final DateTime date; + const _DayBadge({ + required this.isToday, + required this.isInMonth, + required this.date, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Color dayTextColor = Theme.of(context).colorScheme.onBackground; + final String monthString = + DateFormat("MMM ", context.locale.toLanguageTag()).format(date); + final String dayString = date.day.toString(); + + if (!isInMonth) { + dayTextColor = Theme.of(context).disabledColor; + } + if (isToday) { + dayTextColor = Theme.of(context).colorScheme.onPrimary; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (date.day == 1) FlowyText.medium(monthString), + Container( + decoration: BoxDecoration( + color: isToday ? Theme.of(context).colorScheme.primary : null, + borderRadius: Corners.s6Border, + ), + width: isToday ? 26 : null, + height: isToday ? 26 : null, + padding: GridSize.typeOptionContentInsets, + child: Center( + child: FlowyText.medium( + dayString, + color: dayTextColor, + ), + ), + ), + ], + ); + } +} + +class _EventList extends StatelessWidget { + final List events; + final String viewId; + final RowCache rowCache; + final BoxConstraints constraints; + + const _EventList({ + required this.events, + required this.viewId, + required this.rowCache, + required this.constraints, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Flexible( + child: ListView.separated( + itemBuilder: (BuildContext context, int index) => _EventCard( + event: events[index], + viewId: viewId, + rowCache: rowCache, + constraints: constraints, + ), + itemCount: events.length, + padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0), + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + shrinkWrap: true, + ), + ); + } +} + +class _EventCard extends StatelessWidget { + final CalendarDayEvent event; + final String viewId; + final RowCache rowCache; + final BoxConstraints constraints; + + const _EventCard({ + required this.event, + required this.viewId, + required this.rowCache, + required this.constraints, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final rowInfo = rowCache.getRow(event.eventId); + final styles = { + FieldType.Number: NumberCardCellStyle(10), + FieldType.URL: URLCardCellStyle(10), + }; + final cellBuilder = CardCellBuilder( + rowCache.cellCache, + styles: styles, + ); + final renderHook = _calendarEventCardRenderHook(context); + + final card = RowCard( + // Add the key here to make sure the card is rebuilt when the cells + // in this row are updated. + key: ValueKey(event.eventId), + rowMeta: rowInfo!.rowMeta, + viewId: viewId, + rowCache: rowCache, + cardData: event.dateFieldId, + isEditing: false, + cellBuilder: cellBuilder, + openCard: (context) => showEventDetails( + context: context, + event: event.event, + viewId: viewId, + rowCache: rowCache, + ), + styleConfiguration: RowCardStyleConfiguration( + showAccessory: false, + cellPadding: EdgeInsets.zero, + hoverStyle: HoverStyle( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + foregroundColorOnHover: Theme.of(context).colorScheme.onBackground, + ), + ), + renderHook: renderHook, + onStartEditing: () {}, + onEndEditing: () {}, + ); + + final decoration = BoxDecoration( + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).dividerColor), + ), + borderRadius: Corners.s6Border, + ); + + return Draggable( + data: event, + feedback: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth - 16.0, + ), + child: Container( + decoration: decoration.copyWith( + color: AFThemeExtension.of(context).lightGreyHover, + ), + child: card, + ), + ), + child: Container( + decoration: decoration, + child: card, + ), + ); + } + + RowCardRenderHook _calendarEventCardRenderHook(BuildContext context) { + final renderHook = RowCardRenderHook(); + renderHook.addTextCellHook((cellData, primaryFieldId, _) { + if (cellData.isEmpty) { + return const SizedBox.shrink(); + } + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.medium( + cellData, + textAlign: TextAlign.left, + fontSize: 11, + maxLines: null, // Enable multiple lines + ), + ); + }); + + renderHook.addDateCellHook((cellData, cardData, _) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 3, + child: FlowyText.regular( + cellData.date, + fontSize: 10, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + if (cellData.includeTime) + Flexible( + child: FlowyText.regular( + cellData.time, + fontSize: 10, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + ); + }); + + renderHook.addSelectOptionHook((selectedOptions, cardData, _) { + if (selectedOptions.isEmpty) { + return const SizedBox.shrink(); + } + final children = selectedOptions.map( + (option) { + return SelectOptionTag.fromOption( + context: context, + option: option, + ); + }, + ).toList(); + + return IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: SizedBox.expand( + child: Wrap(spacing: 4, runSpacing: 4, children: children), + ), + ), + ); + }); + + return renderHook; + } +} + +class _CardEnterNotifier extends ChangeNotifier { + bool _onEnter = false; + + _CardEnterNotifier(); + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart new file mode 100644 index 0000000000..8dab7e9d34 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -0,0 +1,309 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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 '../../application/row/row_cache.dart'; +import '../../application/row/row_data_controller.dart'; +import '../../widgets/row/cell_builder.dart'; +import '../../widgets/row/row_detail.dart'; +import 'calendar_day.dart'; +import 'layout/sizes.dart'; +import 'toolbar/calendar_setting_bar.dart'; + +class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { + @override + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ) { + return CalendarPage( + key: _makeValueKey(controller), + view: view, + databaseController: controller, + ); + } + + @override + Widget settingBar(BuildContext context, DatabaseController controller) { + return CalendarSettingBar( + key: _makeValueKey(controller), + databaseController: controller, + ); + } + + @override + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ) { + return SizedBox.fromSize(); + } + + ValueKey _makeValueKey(DatabaseController controller) { + return ValueKey(controller.viewId); + } +} + +class CalendarPage extends StatefulWidget { + final ViewPB view; + final DatabaseController databaseController; + const CalendarPage({ + required this.view, + required this.databaseController, + super.key, + }); + + @override + State createState() => _CalendarPageState(); +} + +class _CalendarPageState extends State { + final _eventController = EventController(); + GlobalKey? _calendarState; + late CalendarBloc _calendarBloc; + + @override + void initState() { + _calendarState = GlobalKey(); + _calendarBloc = CalendarBloc( + view: widget.view, + databaseController: widget.databaseController, + )..add(const CalendarEvent.initial()); + + super.initState(); + } + + @override + void dispose() { + _calendarBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CalendarControllerProvider( + controller: _eventController, + child: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: _calendarBloc, + ) + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.initialEvents != c.initialEvents, + listener: (context, state) { + _eventController.removeWhere((_) => true); + _eventController.addAll(state.initialEvents); + }, + ), + BlocListener( + listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds, + listener: (context, state) { + _eventController.removeWhere( + (element) => + state.deleteEventIds.contains(element.event!.eventId), + ); + }, + ), + BlocListener( + listenWhen: (p, c) => p.editingEvent != c.editingEvent, + listener: (context, state) { + if (state.editingEvent != null) { + showEventDetails( + context: context, + event: state.editingEvent!.event!.event, + viewId: widget.view.id, + rowCache: _calendarBloc.rowCache, + ); + } + }, + ), + BlocListener( + // Event create by click the + button or double click on the + // calendar + listenWhen: (p, c) => p.newEvent != c.newEvent, + listener: (context, state) { + if (state.newEvent != null) { + _eventController.add(state.newEvent!); + } + }, + ), + BlocListener( + // When an event is rescheduled + listenWhen: (p, c) => p.updateEvent != c.updateEvent, + listener: (context, state) { + if (state.updateEvent != null) { + _eventController.removeWhere( + (element) => + element.event!.eventId == + state.updateEvent!.event!.eventId, + ); + _eventController.add(state.updateEvent!); + } + }, + ), + ], + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildCalendar( + _eventController, + state.settings + .foldLeft(0, (previous, a) => a.firstDayOfWeek), + ), + ], + ); + }, + ), + ), + ), + ); + } + + Widget _buildCalendar(EventController eventController, int firstDayOfWeek) { + return Expanded( + child: Padding( + padding: GridSize.contentInsets, + child: MonthView( + key: _calendarState, + controller: _eventController, + cellAspectRatio: .6, + startDay: _weekdayFromInt(firstDayOfWeek), + borderColor: Theme.of(context).dividerColor, + headerBuilder: _headerNavigatorBuilder, + weekDayBuilder: _headerWeekDayBuilder, + cellBuilder: _calendarDayBuilder, + ), + ), + ); + } + + Widget _headerNavigatorBuilder(DateTime currentMonth) { + return Row( + children: [ + FlowyText.medium( + DateFormat('MMMM y', context.locale.toLanguageTag()) + .format(currentMonth), + ), + const Spacer(), + FlowyIconButton( + width: CalendarSize.navigatorButtonWidth, + height: CalendarSize.navigatorButtonHeight, + icon: const FlowySvg(name: 'home/arrow_left'), + tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onPressed: () => _calendarState?.currentState?.previousPage(), + ), + FlowyTextButton( + LocaleKeys.calendar_navigation_today.tr(), + fillColor: Colors.transparent, + fontWeight: FontWeight.w500, + tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onPressed: () => + _calendarState?.currentState?.animateToMonth(DateTime.now()), + ), + FlowyIconButton( + width: CalendarSize.navigatorButtonWidth, + height: CalendarSize.navigatorButtonHeight, + icon: const FlowySvg(name: 'home/arrow_right'), + tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onPressed: () => _calendarState?.currentState?.nextPage(), + ), + ], + ); + } + + Widget _headerWeekDayBuilder(day) { + // incoming day starts from Monday, the symbols start from Sunday + final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; + final weekDayString = symbols.WEEKDAYS[(day + 1) % 7]; + return Center( + child: Padding( + padding: CalendarSize.daysOfWeekInsets, + child: FlowyText.medium( + weekDayString, + color: Theme.of(context).hintColor, + ), + ), + ); + } + + Widget _calendarDayBuilder( + DateTime date, + List> calenderEvents, + isToday, + isInMonth, + ) { + final events = calenderEvents.map((value) => value.event!).toList(); + // Sort the events by timestamp. Because the database view is not + // reserving the order of the events. Reserving the order of the rows/events + // is implemnted in the develop branch(WIP). Will be replaced with that. + events.sort( + (a, b) => a.event.timestamp.compareTo(b.event.timestamp), + ); + return CalendarDayCard( + viewId: widget.view.id, + isToday: isToday, + isInMonth: isInMonth, + events: events, + date: date, + rowCache: _calendarBloc.rowCache, + onCreateEvent: (date) { + _calendarBloc.add( + CalendarEvent.createEvent( + date, + LocaleKeys.calendar_defaultNewCalendarTitle.tr(), + ), + ); + }, + ); + } + + WeekDays _weekdayFromInt(int dayOfWeek) { + // dayOfWeek starts from Sunday, WeekDays starts from Monday + return WeekDays.values[(dayOfWeek - 1) % 7]; + } +} + +void showEventDetails({ + required BuildContext context, + required CalendarEventPB event, + required String viewId, + required RowCache rowCache, +}) { + final dataController = RowController( + rowMeta: event.rowMeta, + viewId: viewId, + rowCache: rowCache, + ); + + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: GridCellBuilder( + cellCache: rowCache.cellCache, + ), + rowController: dataController, + ); + }, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/layout/sizes.dart new file mode 100644 index 0000000000..4e9d68f821 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/layout/sizes.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +class CalendarSize { + static double scale = 1; + + static double get scrollBarSize => 12 * scale; + static double get navigatorButtonWidth => 20 * scale; + static double get navigatorButtonHeight => 25 * scale; + static EdgeInsets get daysOfWeekInsets => + EdgeInsets.symmetric(vertical: 10.0 * scale); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart new file mode 100644 index 0000000000..79f718f9e2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -0,0 +1,449 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/startup/startup.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/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:protobuf/protobuf.dart'; + +abstract class ICalendarSetting { + /// Returns the current layout settings for the calendar view. + CalendarLayoutSettingPB? getLayoutSetting(); + + /// Updates the layout settings for the calendar view. + void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings); +} + +/// Widget that displays a list of settings that alters the appearance of the +/// calendar +class CalendarLayoutSetting extends StatefulWidget { + final String viewId; + final FieldController fieldController; + final ICalendarSetting calendarSettingController; + + const CalendarLayoutSetting({ + required this.viewId, + required this.fieldController, + required this.calendarSettingController, + super.key, + }); + + @override + State createState() => _CalendarLayoutSettingState(); +} + +class _CalendarLayoutSettingState extends State { + late final PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return CalendarSettingBloc( + viewId: widget.viewId, + layoutSettings: widget.calendarSettingController.getLayoutSetting(), + )..add( + const CalendarSettingEvent.init(), + ); + }, + child: BlocBuilder( + builder: (context, state) { + final CalendarLayoutSettingPB? settings = state.layoutSetting + .foldLeft(null, (previous, settings) => settings); + + if (settings == null) { + return const CircularProgressIndicator(); + } + final availableSettings = _availableCalendarSettings(settings); + final items = availableSettings.map((setting) { + switch (setting) { + case CalendarLayoutSettingAction.showWeekNumber: + return ShowWeekNumber( + showWeekNumbers: settings.showWeekNumbers, + onUpdated: (showWeekNumbers) { + _updateLayoutSettings( + context, + showWeekNumbers: showWeekNumbers, + ); + }, + ); + case CalendarLayoutSettingAction.showWeekends: + return ShowWeekends( + showWeekends: settings.showWeekends, + onUpdated: (showWeekends) { + _updateLayoutSettings( + context, + showWeekends: showWeekends, + ); + }, + ); + case CalendarLayoutSettingAction.firstDayOfWeek: + return FirstDayOfWeek( + firstDayOfWeek: settings.firstDayOfWeek, + popoverMutex: popoverMutex, + onUpdated: (firstDayOfWeek) { + _updateLayoutSettings( + context, + firstDayOfWeek: firstDayOfWeek, + ); + }, + ); + case CalendarLayoutSettingAction.layoutField: + return LayoutDateField( + fieldController: widget.fieldController, + viewId: widget.viewId, + fieldId: settings.fieldId, + popoverMutex: popoverMutex, + onUpdated: (fieldId) { + _updateLayoutSettings( + context, + layoutFieldId: fieldId, + ); + }, + ); + default: + return const SizedBox(); + } + }).toList(); + + return SizedBox( + width: 200, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: items.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) => items[index], + padding: const EdgeInsets.all(6.0), + ), + ); + }, + ), + ); + } + + List _availableCalendarSettings( + CalendarLayoutSettingPB layoutSettings, + ) { + final List settings = [ + CalendarLayoutSettingAction.layoutField, + // CalendarLayoutSettingAction.layoutType, + // CalendarLayoutSettingAction.showWeekNumber, + ]; + + switch (layoutSettings.layoutTy) { + case CalendarLayoutPB.DayLayout: + // settings.add(CalendarLayoutSettingAction.showTimeLine); + break; + case CalendarLayoutPB.MonthLayout: + settings.addAll([ + // CalendarLayoutSettingAction.showWeekends, + // if (layoutSettings.showWeekends) + CalendarLayoutSettingAction.firstDayOfWeek, + ]); + break; + case CalendarLayoutPB.WeekLayout: + settings.addAll([ + // CalendarLayoutSettingAction.showWeekends, + // if (layoutSettings.showWeekends) + CalendarLayoutSettingAction.firstDayOfWeek, + // CalendarLayoutSettingAction.showTimeLine, + ]); + break; + } + + return settings; + } + + void _updateLayoutSettings( + BuildContext context, { + bool? showWeekends, + bool? showWeekNumbers, + int? firstDayOfWeek, + String? layoutFieldId, + }) { + CalendarLayoutSettingPB setting = context + .read() + .state + .layoutSetting + .foldLeft(null, (previous, settings) => settings)!; + setting.freeze(); + setting = setting.rebuild((setting) { + if (showWeekends != null) { + setting.showWeekends = !showWeekends; + } + if (showWeekNumbers != null) { + setting.showWeekNumbers = !showWeekNumbers; + } + if (firstDayOfWeek != null) { + setting.firstDayOfWeek = firstDayOfWeek; + } + if (layoutFieldId != null) { + setting.fieldId = layoutFieldId; + } + }); + context + .read() + .add(CalendarSettingEvent.updateLayoutSetting(setting)); + + widget.calendarSettingController.updateLayoutSettings(setting); + } +} + +class LayoutDateField extends StatelessWidget { + final String fieldId; + final String viewId; + final FieldController fieldController; + final PopoverMutex popoverMutex; + final Function(String fieldId) onUpdated; + + const LayoutDateField({ + required this.fieldId, + required this.fieldController, + required this.viewId, + required this.popoverMutex, + required this.onUpdated, + super.key, + }); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(300, 400)), + mutex: popoverMutex, + offset: const Offset(-16, 0), + popupBuilder: (context) { + return BlocProvider( + create: (context) => getIt( + param1: viewId, + param2: fieldController, + )..add(const DatabasePropertyEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final items = state.fieldContexts + .where((field) => field.fieldType == FieldType.DateTime) + .map( + (fieldInfo) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(fieldInfo.name), + onTap: () { + onUpdated(fieldInfo.id); + popoverMutex.close(); + }, + leftIcon: const FlowySvg(name: 'grid/field/date'), + rightIcon: fieldInfo.id == fieldId + ? const FlowySvg(name: 'grid/checkmark') + : null, + ), + ); + }, + ).toList(); + + return SizedBox( + width: 200, + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => items[index], + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: items.length, + ), + ); + }, + ), + ); + }, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + text: FlowyText.medium( + LocaleKeys.calendar_settings_layoutDateField.tr(), + ), + ), + ), + ); + } +} + +class ShowWeekNumber extends StatelessWidget { + final bool showWeekNumbers; + final Function(bool showWeekNumbers) onUpdated; + + const ShowWeekNumber({ + required this.showWeekNumbers, + required this.onUpdated, + super.key, + }); + + @override + Widget build(BuildContext context) { + return _toggleItem( + onToggle: (showWeekNumbers) { + onUpdated(!showWeekNumbers); + }, + value: showWeekNumbers, + text: LocaleKeys.calendar_settings_showWeekNumbers.tr(), + ); + } +} + +class ShowWeekends extends StatelessWidget { + final bool showWeekends; + final Function(bool showWeekends) onUpdated; + const ShowWeekends({ + super.key, + required this.showWeekends, + required this.onUpdated, + }); + + @override + Widget build(BuildContext context) { + return _toggleItem( + onToggle: (showWeekends) { + onUpdated(!showWeekends); + }, + value: showWeekends, + text: LocaleKeys.calendar_settings_showWeekends.tr(), + ); + } +} + +class FirstDayOfWeek extends StatelessWidget { + final int firstDayOfWeek; + final PopoverMutex popoverMutex; + final Function(int firstDayOfWeek) onUpdated; + const FirstDayOfWeek({ + super.key, + required this.firstDayOfWeek, + required this.onUpdated, + required this.popoverMutex, + }); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(300, 400)), + mutex: popoverMutex, + offset: const Offset(-16, 0), + popupBuilder: (context) { + final symbols = + DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; + // starts from sunday + const len = 2; + final items = symbols.WEEKDAYS.take(len).indexed.map((entry) { + return StartFromButton( + title: entry.$2, + dayIndex: entry.$1, + isSelected: firstDayOfWeek == entry.$1, + onTap: (index) { + onUpdated(index); + popoverMutex.close(); + }, + ); + }).toList(); + + return SizedBox( + width: 100, + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => items[index], + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: len, + ), + ); + }, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + text: FlowyText.medium( + LocaleKeys.calendar_settings_firstDayOfWeek.tr(), + ), + ), + ), + ); + } +} + +Widget _toggleItem({ + required String text, + required bool value, + required void Function(bool) onToggle, +}) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + child: Row( + children: [ + FlowyText.medium(text), + const Spacer(), + Toggle( + value: value, + onChanged: (value) => onToggle(!value), + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], + ), + ), + ); +} + +enum CalendarLayoutSettingAction { + layoutField, + layoutType, + showWeekends, + firstDayOfWeek, + showWeekNumber, + showTimeLine, +} + +class StartFromButton extends StatelessWidget { + final int dayIndex; + final String title; + final bool isSelected; + final void Function(int) onTap; + const StartFromButton({ + required this.title, + required this.dayIndex, + required this.onTap, + required this.isSelected, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(title), + onTap: () => onTap(dayIndex), + rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart new file mode 100644 index 0000000000..ec74f739e6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart @@ -0,0 +1,178 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_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/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CalendarSettingBar extends StatelessWidget { + final DatabaseController databaseController; + const CalendarSettingBar({ + required this.databaseController, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + UnscheduleEventsButton(databaseController: databaseController), + SettingButton( + databaseController: databaseController, + ), + ], + ), + ); + } +} + +class UnscheduleEventsButton extends StatefulWidget { + final DatabaseController databaseController; + const UnscheduleEventsButton({ + required this.databaseController, + Key? key, + }) : super(key: key); + + @override + State createState() => _UnscheduleEventsButtonState(); +} + +class _UnscheduleEventsButtonState extends State { + late final PopoverController _popoverController; + late final UnscheduleEventsBloc _bloc; + + @override + void initState() { + super.initState(); + _bloc = UnscheduleEventsBloc(databaseController: widget.databaseController) + ..add(const UnscheduleEventsEvent.initial()); + _popoverController = PopoverController(); + } + + @override + dispose() { + _bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.bottomWithCenterAligned, + controller: _popoverController, + offset: const Offset(0, 8), + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600), + child: BlocProvider.value( + value: _bloc, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.unscheduleEvents.length != + current.unscheduleEvents.length, + builder: (context, state) { + return FlowyTextButton( + "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})", + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.typeOptionContentInsets, + ); + }, + ), + ), + popupBuilder: (context) { + return UnscheduleEventsList( + viewId: _bloc.viewId, + rowCache: _bloc.rowCache, + controller: _popoverController, + unscheduleEvents: _bloc.state.unscheduleEvents, + ); + }, + ); + } +} + +class UnscheduleEventsList extends StatelessWidget { + final String viewId; + final RowCache rowCache; + final PopoverController controller; + final List unscheduleEvents; + const UnscheduleEventsList({ + required this.viewId, + required this.controller, + required this.unscheduleEvents, + required this.rowCache, + super.key, + }); + + @override + Widget build(BuildContext context) { + final cells = [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: FlowyText.medium( + LocaleKeys.calendar_settings_clickToAdd.tr(), + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + const VSpace(6), + ...unscheduleEvents.map( + (e) => UnscheduledEventCell( + event: e, + onPressed: () { + showEventDetails( + context: context, + event: e, + viewId: viewId, + rowCache: rowCache, + ); + controller.close(); + }, + ), + ) + ]; + + return ListView.separated( + itemBuilder: (context, index) => cells[index], + itemCount: cells.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + shrinkWrap: true, + ); + } +} + +class UnscheduledEventCell extends StatelessWidget { + final CalendarEventPB event; + final VoidCallback onPressed; + const UnscheduledEventCell({ + required this.event, + required this.onPressed, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium( + event.title.isEmpty + ? LocaleKeys.calendar_defaultNewCalendarTitle.tr() + : event.title, + ), + onTap: onPressed, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checkbox_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checkbox_filter_editor_bloc.dart new file mode 100644 index 0000000000..6b94863f45 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checkbox_filter_editor_bloc.dart @@ -0,0 +1,102 @@ +import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart'; +import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; +import 'package:appflowy/plugins/database_view/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'; +import 'dart:async'; + +part 'checkbox_filter_editor_bloc.freezed.dart'; + +class CheckboxFilterEditorBloc + extends Bloc { + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + CheckboxFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(CheckboxFilterEditorState.initial(filterInfo)) { + on( + (event, emit) async { + 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, + fieldType: filterInfo.fieldInfo.fieldType, + ); + }, + 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( + onDeleted: () { + if (!isClosed) add(const CheckboxFilterEditorEvent.delete()); + }, + 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_view/grid/application/filter/checklist_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart new file mode 100644 index 0000000000..a972911e99 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart @@ -0,0 +1,107 @@ +import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart'; +import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; +import 'package:appflowy/plugins/database_view/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'; +import 'dart:async'; + +part 'checklist_filter_bloc.freezed.dart'; + +class ChecklistFilterEditorBloc + extends Bloc { + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + ChecklistFilterEditorBloc({ + required this.filterInfo, + }) : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(ChecklistFilterEditorState.initial(filterInfo)) { + on( + (event, emit) async { + 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, + fieldType: filterInfo.fieldInfo.fieldType, + ); + }, + 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( + onDeleted: () { + if (!isClosed) add(const ChecklistFilterEditorEvent.delete()); + }, + 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_view/grid/application/filter/filter_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart new file mode 100644 index 0000000000..96d3d9e563 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart @@ -0,0 +1,190 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.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:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'filter_create_bloc.freezed.dart'; + +class GridCreateFilterBloc + extends Bloc { + final String viewId; + final FilterBackendService _filterBackendSvc; + final FieldController fieldController; + void Function(List)? _onFieldFn; + GridCreateFilterBloc({required this.viewId, required this.fieldController}) + : _filterBackendSvc = FilterBackendService(viewId: viewId), + super(GridCreateFilterState.initial(fieldController.fieldInfos)) { + on( + (event, emit) async { + event.when( + initial: () async { + _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: SelectOptionConditionPB.OptionIs, + 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, + content: "", + ); + case FieldType.RichText: + return _filterBackendSvc.insertTextFilter( + fieldId: fieldId, + condition: TextFilterConditionPB.Contains, + content: '', + ); + case FieldType.SingleSelect: + return _filterBackendSvc.insertSelectOptionFilter( + fieldId: fieldId, + condition: SelectOptionConditionPB.OptionIs, + fieldType: FieldType.SingleSelect, + ); + case FieldType.URL: + return _filterBackendSvc.insertURLFilter( + fieldId: fieldId, + condition: TextFilterConditionPB.Contains, + ); + } + + return left(unit); + } + + @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_view/grid/application/filter/filter_menu_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_menu_bloc.dart new file mode 100644 index 0000000000..76b135db1d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_menu_bloc.dart @@ -0,0 +1,122 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'filter_menu_bloc.freezed.dart'; + +class GridFilterMenuBloc + extends Bloc { + final String viewId; + final FieldController fieldController; + void Function(List)? _onFilterFn; + void Function(List)? _onFieldFn; + + GridFilterMenuBloc({required this.viewId, required this.fieldController}) + : super( + GridFilterMenuState.initial( + viewId, + fieldController.filterInfos, + fieldController.fieldInfos, + ), + ) { + 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(GridFilterMenuEvent.didReceiveFilters(filters)); + }; + + _onFieldFn = (fields) { + add(GridFilterMenuEvent.didReceiveFields(fields)); + }; + + fieldController.addListener( + onFilters: (filters) { + _onFilterFn?.call(filters); + }, + onReceiveFields: (fields) { + _onFieldFn?.call(fields); + }, + ); + } + + @override + Future close() { + if (_onFilterFn != null) { + fieldController.removeListener(onFiltersListener: _onFilterFn!); + _onFilterFn = null; + } + if (_onFieldFn != null) { + fieldController.removeListener(onFieldsListener: _onFieldFn!); + _onFieldFn = null; + } + return super.close(); + } +} + +@freezed +class GridFilterMenuEvent with _$GridFilterMenuEvent { + const factory GridFilterMenuEvent.initial() = _Initial; + const factory GridFilterMenuEvent.didReceiveFilters( + List filters, + ) = _DidReceiveFilters; + const factory GridFilterMenuEvent.didReceiveFields(List fields) = + _DidReceiveFields; + const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility; +} + +@freezed +class GridFilterMenuState with _$GridFilterMenuState { + const factory GridFilterMenuState({ + required String viewId, + required List filters, + required List fields, + required List creatableFields, + required bool isVisible, + }) = _GridFilterMenuState; + + factory GridFilterMenuState.initial( + String viewId, + List filterInfos, + List fields, + ) => + GridFilterMenuState( + 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_view/grid/application/filter/select_option_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart new file mode 100644 index 0000000000..2e459699da --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart @@ -0,0 +1,146 @@ +import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart'; +import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart'; +import 'package:appflowy/plugins/database_view/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'; +import 'dart:async'; + +part 'select_option_filter_bloc.freezed.dart'; + +class SelectOptionFilterEditorBloc + extends Bloc { + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + final SelectOptionFilterDelegate delegate; + + 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)) { + on( + (event, emit) async { + event.when( + initial: () async { + _startListening(); + _loadOptions(); + }, + updateCondition: (SelectOptionConditionPB 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, + fieldType: filterInfo.fieldInfo.fieldType, + ); + }, + 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( + onDeleted: () { + if (!isClosed) add(const SelectOptionFilterEditorEvent.delete()); + }, + onUpdated: (filter) { + if (!isClosed) { + add(SelectOptionFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + void _loadOptions() { + delegate.loadOptions().then((options) { + if (!isClosed) { + 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( + SelectOptionConditionPB 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_view/grid/application/filter/select_option_filter_list_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart new file mode 100644 index 0000000000..b20390044f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart @@ -0,0 +1,153 @@ +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.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 { + final SelectOptionFilterDelegate delegate; + SelectOptionFilterListBloc({ + required String viewId, + required FieldPB fieldPB, + required this.delegate, + required List selectedOptionIds, + }) : super(SelectOptionFilterListState.initial(selectedOptionIds)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + _loadOptions(); + }, + selectOption: (option) { + final selectedOptionIds = Set.from(state.selectedOptionIds); + selectedOptionIds.add(option.id); + + _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() { + delegate.loadOptions().then((options) { + if (!isClosed) { + add(SelectOptionFilterListEvent.didReceiveOptions(options)); + } + }); + } + + void _startListening() {} +} + +@freezed +class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent { + const factory SelectOptionFilterListEvent.initial() = _Initial; + const factory SelectOptionFilterListEvent.selectOption( + SelectOptionPB option, + ) = _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 { + final SelectOptionPB optionPB; + final bool isSelected; + + VisibleSelectOption(this.optionPB, this.isSelected); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/text_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/text_filter_editor_bloc.dart new file mode 100644 index 0000000000..bac833781e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/text_filter_editor_bloc.dart @@ -0,0 +1,113 @@ +import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart'; +import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_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'; +import 'dart:async'; + +part 'text_filter_editor_bloc.freezed.dart'; + +class TextFilterEditorBloc + extends Bloc { + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + TextFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(TextFilterEditorState.initial(filterInfo)) { + on( + (event, emit) async { + event.when( + initial: () async { + _startListening(); + }, + updateCondition: (TextFilterConditionPB condition) { + _filterBackendSvc.insertTextFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ); + }, + updateContent: (content) { + _filterBackendSvc.insertTextFilter( + 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, + fieldType: filterInfo.fieldInfo.fieldType, + ); + }, + 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( + onDeleted: () { + if (!isClosed) add(const TextFilterEditorEvent.delete()); + }, + 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/grid_accessory_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart similarity index 92% rename from frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_accessory_bloc.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart index 11495c87d5..199c4e2252 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_accessory_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart @@ -5,8 +5,14 @@ part 'grid_accessory_bloc.freezed.dart'; class DatabaseViewSettingExtensionBloc extends Bloc< DatabaseViewSettingExtensionEvent, DatabaseViewSettingExtensionState> { + final String viewId; + DatabaseViewSettingExtensionBloc({required this.viewId}) - : super(DatabaseViewSettingExtensionState.initial(viewId)) { + : super( + DatabaseViewSettingExtensionState.initial( + viewId, + ), + ) { on( (event, emit) async { event.when( @@ -18,8 +24,6 @@ class DatabaseViewSettingExtensionBloc extends Bloc< }, ); } - - final String viewId; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart new file mode 100644 index 0000000000..4907611004 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../application/field/field_controller.dart'; +import '../../application/database_controller.dart'; +import 'dart:collection'; + +part 'grid_bloc.freezed.dart'; + +class GridBloc extends Bloc { + final DatabaseController databaseController; + + GridBloc({required ViewPB view, required this.databaseController}) + : super(GridState.initial(view.id)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + await _openGrid(emit); + }, + createRow: () { + databaseController.createRow(); + }, + deleteRow: (rowInfo) async { + final rowService = RowBackendService( + viewId: rowInfo.viewId, + ); + await rowService.deleteRow(rowInfo.rowId); + }, + moveRow: (int from, int to) { + final List rows = [...state.rowInfos]; + + final fromRow = rows[from].rowId; + final toRow = rows[to].rowId; + + rows.insert(to, rows.removeAt(from)); + emit(state.copyWith(rowInfos: rows)); + + databaseController.moveRow(fromRowId: fromRow, toRowId: toRow); + }, + didReceiveGridUpdate: (grid) { + emit(state.copyWith(grid: Some(grid))); + }, + didReceiveFieldUpdate: (fields) { + emit( + state.copyWith( + fields: GridFieldEquatable(fields), + ), + ); + }, + didLoadRows: (newRowInfos, reason) { + emit( + state.copyWith( + rowInfos: newRowInfos, + rowCount: newRowInfos.length, + reason: reason, + ), + ); + }, + didReceveFilters: (List filters) { + emit( + state.copyWith( + reorderable: filters.isEmpty && state.sorts.isEmpty, + filters: filters, + ), + ); + }, + didReceveSorts: (List sorts) { + emit( + state.copyWith( + reorderable: sorts.isEmpty && state.filters.isEmpty, + sorts: sorts, + ), + ); + }, + ); + }, + ); + } + + RowCache getRowCache(RowId rowId) { + return databaseController.rowCache; + } + + void _startListening() { + final onDatabaseChanged = DatabaseCallbacks( + onDatabaseChanged: (database) { + if (!isClosed) { + add(GridEvent.didReceiveGridUpdate(database)); + } + }, + onNumOfRowsChanged: (rowInfos, _, reason) { + if (!isClosed) { + add(GridEvent.didLoadRows(rowInfos, reason)); + } + }, + onRowsUpdated: (rows, reason) { + if (!isClosed) { + add( + GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason), + ); + } + }, + onFieldsChanged: (fields) { + if (!isClosed) { + add(GridEvent.didReceiveFieldUpdate(fields)); + } + }, + onFiltersChanged: (filters) { + if (!isClosed) { + add(GridEvent.didReceveFilters(filters)); + } + }, + onSortsChanged: (sorts) { + if (!isClosed) { + add(GridEvent.didReceveSorts(sorts)); + } + }, + ); + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); + } + + Future _openGrid(Emitter emit) async { + final result = await databaseController.open(); + result.fold( + (grid) { + emit( + state.copyWith(loadingState: GridLoadingState.finish(left(unit))), + ); + }, + (err) => emit( + state.copyWith(loadingState: GridLoadingState.finish(right(err))), + ), + ); + } +} + +@freezed +class GridEvent with _$GridEvent { + const factory GridEvent.initial() = InitialGrid; + const factory GridEvent.createRow() = _CreateRow; + const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow; + const factory GridEvent.moveRow(int from, int to) = _MoveRow; + const factory GridEvent.didLoadRows( + List rows, + RowsChangedReason reason, + ) = _DidReceiveRowUpdate; + const factory GridEvent.didReceiveFieldUpdate( + List fields, + ) = _DidReceiveFieldUpdate; + + const factory GridEvent.didReceiveGridUpdate( + DatabasePB grid, + ) = _DidReceiveGridUpdate; + + const factory GridEvent.didReceveFilters(List filters) = + _DidReceiveFilters; + const factory GridEvent.didReceveSorts(List sorts) = + _DidReceiveSorts; +} + +@freezed +class GridState with _$GridState { + const factory GridState({ + required String viewId, + required Option grid, + required GridFieldEquatable fields, + required List rowInfos, + required int rowCount, + required GridLoadingState loadingState, + required bool reorderable, + required RowsChangedReason reason, + required List sorts, + required List filters, + }) = _GridState; + + factory GridState.initial(String viewId) => GridState( + fields: GridFieldEquatable(UnmodifiableListView([])), + rowInfos: [], + rowCount: 0, + grid: none(), + viewId: viewId, + reorderable: true, + loadingState: const _Loading(), + reason: const InitialListState(), + filters: [], + sorts: [], + ); +} + +@freezed +class GridLoadingState with _$GridLoadingState { + const factory GridLoadingState.loading() = _Loading; + const factory GridLoadingState.finish( + Either successOrFail, + ) = _Finish; +} + +class GridFieldEquatable extends Equatable { + final List _fields; + const GridFieldEquatable( + List fields, + ) : _fields = fields; + + @override + List get props { + if (_fields.isEmpty) { + return []; + } + + return [ + _fields.length, + _fields + .map((field) => field.width) + .reduce((value, element) => value + element), + ]; + } + + UnmodifiableListView get value => UnmodifiableListView(_fields); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart new file mode 100644 index 0000000000..c057486b77 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart @@ -0,0 +1,90 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import '../../application/field/field_service.dart'; + +part 'grid_header_bloc.freezed.dart'; + +class GridHeaderBloc extends Bloc { + final FieldController fieldController; + final String viewId; + + GridHeaderBloc({ + required this.viewId, + required this.fieldController, + }) : super(GridHeaderState.initial(fieldController.fieldInfos)) { + on( + (event, emit) async { + await event.map( + initial: (_InitialHeader value) async { + _startListening(); + }, + didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) { + emit( + state.copyWith( + fields: value.fields + .where((element) => element.visibility) + .toList(), + ), + ); + }, + moveField: (_MoveField value) async { + await _moveField(value, emit); + }, + ); + }, + ); + } + + Future _moveField( + _MoveField value, + Emitter emit, + ) async { + final fields = List.from(state.fields); + fields.insert(value.toIndex, fields.removeAt(value.fromIndex)); + emit(state.copyWith(fields: fields)); + + final fieldService = + FieldBackendService(viewId: viewId, fieldId: value.field.id); + final result = await fieldService.moveField( + value.fromIndex, + value.toIndex, + ); + result.fold((l) {}, (err) => Log.error(err)); + } + + Future _startListening() async { + fieldController.addListener( + onReceiveFields: (fields) => + add(GridHeaderEvent.didReceiveFieldUpdate(fields)), + listenWhen: () => !isClosed, + ); + } +} + +@freezed +class GridHeaderEvent with _$GridHeaderEvent { + const factory GridHeaderEvent.initial() = _InitialHeader; + const factory GridHeaderEvent.didReceiveFieldUpdate(List fields) = + _DidReceiveFieldUpdate; + const factory GridHeaderEvent.moveField( + FieldPB field, + int fromIndex, + int toIndex, + ) = _MoveField; +} + +@freezed +class GridHeaderState with _$GridHeaderState { + const factory GridHeaderState({required List fields}) = + _GridHeaderState; + + factory GridHeaderState.initial(List fields) { + // final List newFields = List.from(fields); + // newFields.retainWhere((field) => field.visibility); + return GridHeaderState(fields: fields); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart new file mode 100644 index 0000000000..a741f0dd09 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart @@ -0,0 +1,56 @@ +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +import '../../../application/row/row_service.dart'; + +part 'row_action_sheet_bloc.freezed.dart'; + +class RowActionSheetBloc + extends Bloc { + final RowBackendService _rowService; + + RowActionSheetBloc({ + required String viewId, + required RowId rowId, + }) : _rowService = RowBackendService(viewId: viewId), + super(RowActionSheetState.initial(rowId)) { + on( + (event, emit) async { + await event.when( + deleteRow: () async { + final result = await _rowService.deleteRow(state.rowId); + logResult(result); + }, + duplicateRow: () async { + final result = await _rowService.duplicateRow(rowId: state.rowId); + logResult(result); + }, + ); + }, + ); + } + + void logResult(Either result) { + result.fold((l) => null, (err) => Log.error(err)); + } +} + +@freezed +class RowActionSheetEvent with _$RowActionSheetEvent { + const factory RowActionSheetEvent.duplicateRow() = _DuplicateRow; + const factory RowActionSheetEvent.deleteRow() = _DeleteRow; +} + +@freezed +class RowActionSheetState with _$RowActionSheetState { + const factory RowActionSheetState({ + required RowId rowId, + }) = _RowActionSheetState; + + factory RowActionSheetState.initial(RowId rowId) => RowActionSheetState( + rowId: rowId, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart new file mode 100644 index 0000000000..0a108abbdf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart @@ -0,0 +1,114 @@ +import 'dart:collection'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import '../../../application/cell/cell_service.dart'; +import '../../../application/field/field_controller.dart'; +import '../../../application/row/row_cache.dart'; +import '../../../application/row/row_data_controller.dart'; +import '../../../application/row/row_service.dart'; + +part 'row_bloc.freezed.dart'; + +class RowBloc extends Bloc { + final RowBackendService _rowBackendSvc; + final RowController _dataController; + final String viewId; + final String rowId; + + RowBloc({ + required this.rowId, + required this.viewId, + required RowController dataController, + }) : _rowBackendSvc = RowBackendService(viewId: viewId), + _dataController = dataController, + super(RowState.initial(dataController.loadData())) { + on( + (event, emit) async { + await event.when( + initial: () async { + await _startListening(); + }, + createRow: () { + _rowBackendSvc.createRowAfterRow(rowId); + }, + didReceiveCells: (cellByFieldId, reason) async { + final cells = cellByFieldId.values + .map((e) => GridCellEquatable(e.fieldInfo)) + .toList(); + emit( + state.copyWith( + cellByFieldId: cellByFieldId, + cells: UnmodifiableListView(cells), + changeReason: reason, + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + _dataController.dispose(); + return super.close(); + } + + Future _startListening() async { + _dataController.addListener( + onRowChanged: (cells, reason) { + if (!isClosed) { + add(RowEvent.didReceiveCells(cells, reason)); + } + }, + ); + } +} + +@freezed +class RowEvent with _$RowEvent { + const factory RowEvent.initial() = _InitialRow; + const factory RowEvent.createRow() = _CreateRow; + const factory RowEvent.didReceiveCells( + CellContextByFieldId cellsByFieldId, + RowsChangedReason reason, + ) = _DidReceiveCells; +} + +@freezed +class RowState with _$RowState { + const factory RowState({ + required CellContextByFieldId cellByFieldId, + required UnmodifiableListView cells, + RowsChangedReason? changeReason, + }) = _RowState; + + factory RowState.initial( + CellContextByFieldId cellByFieldId, + ) => + RowState( + cellByFieldId: cellByFieldId, + cells: UnmodifiableListView( + cellByFieldId.values + .map((e) => GridCellEquatable(e.fieldInfo)) + .toList(), + ), + ); +} + +class GridCellEquatable extends Equatable { + final FieldInfo _fieldContext; + + const GridCellEquatable(FieldInfo field) : _fieldContext = field; + + @override + List get props => [ + _fieldContext.id, + _fieldContext.fieldType, + _fieldContext.visibility, + _fieldContext.width, + ]; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart new file mode 100644 index 0000000000..812a98b837 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart @@ -0,0 +1,104 @@ +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import '../../../application/cell/cell_service.dart'; +import '../../../application/field/field_service.dart'; +import '../../../application/row/row_data_controller.dart'; +part 'row_detail_bloc.freezed.dart'; + +class RowDetailBloc extends Bloc { + final RowBackendService rowService; + final RowController dataController; + + RowDetailBloc({ + required this.dataController, + }) : rowService = RowBackendService(viewId: dataController.viewId), + super(RowDetailState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + await _startListening(); + final cells = dataController.loadData(); + if (!isClosed) { + add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); + } + }, + didReceiveCellDatas: (cells) { + emit(state.copyWith(cells: cells)); + }, + deleteField: (fieldId) { + _fieldBackendService(fieldId).deleteField(); + }, + hideField: (fieldId) async { + final result = await _fieldBackendService(fieldId).updateField( + visibility: false, + ); + result.fold( + (l) {}, + (err) => Log.error(err), + ); + }, + deleteRow: (rowId) async { + await rowService.deleteRow(rowId); + }, + duplicateRow: (String rowId, String? groupId) async { + await rowService.duplicateRow( + rowId: rowId, + groupId: groupId, + ); + }, + ); + }, + ); + } + + @override + Future close() async { + dataController.dispose(); + return super.close(); + } + + Future _startListening() async { + dataController.addListener( + onRowChanged: (cells, reason) { + if (!isClosed) { + add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); + } + }, + ); + } + + FieldBackendService _fieldBackendService(String fieldId) { + return FieldBackendService( + viewId: dataController.viewId, + fieldId: fieldId, + ); + } +} + +@freezed +class RowDetailEvent with _$RowDetailEvent { + const factory RowDetailEvent.initial() = _Initial; + const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField; + const factory RowDetailEvent.hideField(String fieldId) = _HideField; + const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow; + const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) = + _DuplicateRow; + const factory RowDetailEvent.didReceiveCellDatas( + List gridCells, + ) = _DidReceiveCellDatas; +} + +@freezed +class RowDetailState with _$RowDetailState { + const factory RowDetailState({ + required List cells, + }) = _RowDetailState; + + factory RowDetailState.initial() => RowDetailState( + cells: List.empty(), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart similarity index 85% rename from frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart index 743d854b23..2d6aced74e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart @@ -1,37 +1,31 @@ -import 'package:flutter/foundation.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import '../../../application/row/row_service.dart'; part 'row_document_bloc.freezed.dart'; class RowDocumentBloc extends Bloc { + final String rowId; + final RowBackendService _rowBackendSvc; + RowDocumentBloc({ required this.rowId, required String viewId, }) : _rowBackendSvc = RowBackendService(viewId: viewId), super(RowDocumentState.initial()) { - _dispatch(); - } - - final String rowId; - final RowBackendService _rowBackendSvc; - - void _dispatch() { on( (event, emit) async { await event.when( - initial: () { + initial: () async { _getRowDocumentView(); }, didReceiveRowDocument: (view) { @@ -49,14 +43,6 @@ class RowDocumentBloc extends Bloc { ), ); }, - updateIsEmpty: (isEmpty) async { - final unitOrFailure = await _rowBackendSvc.updateMeta( - rowId: rowId, - isDocumentEmpty: isEmpty, - ); - - unitOrFailure.fold((l) => null, (err) => Log.error(err)); - }, ); }, ); @@ -76,12 +62,12 @@ class RowDocumentBloc extends Bloc { viewsOrError.fold( (view) => add(RowDocumentEvent.didReceiveRowDocument(view)), (error) async { - if (error.code == ErrorCode.RecordNotFound) { + if (error.code == ErrorCode.RecordNotFound.value) { // By default, the document of the row is not exist. So creating a // 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 { @@ -118,8 +104,6 @@ class RowDocumentEvent with _$RowDocumentEvent { _DidReceiveRowDocument; const factory RowDocumentEvent.didReceiveError(FlowyError error) = _DidReceiveError; - const factory RowDocumentEvent.updateIsEmpty(bool isDocumentEmpty) = - _UpdateIsEmpty; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_create_bloc.dart new file mode 100644 index 0000000000..f189972722 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_create_bloc.dart @@ -0,0 +1,133 @@ +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbserver.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import '../../../application/field/field_controller.dart'; +import '../../../application/sort/sort_service.dart'; +import 'util.dart'; + +part 'sort_create_bloc.freezed.dart'; + +class CreateSortBloc extends Bloc { + final String viewId; + final SortBackendService _sortBackendSvc; + final FieldController fieldController; + void Function(List)? _onFieldFn; + CreateSortBloc({required this.viewId, required this.fieldController}) + : _sortBackendSvc = SortBackendService(viewId: viewId), + super(CreateSortState.initial(fieldController.fieldInfos)) { + on( + (event, emit) async { + event.when( + initial: () async { + _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), + ), + ); + }, + createDefaultSort: (FieldInfo field) { + emit(state.copyWith(didCreateSort: true)); + _createDefaultSort(field); + }, + ); + }, + ); + } + + List _filterFields( + List fields, + String filterText, + ) { + final List allFields = List.from(fields); + final keyword = filterText.toLowerCase(); + allFields.retainWhere((field) { + if (!field.canCreateSort) { + return false; + } + + if (filterText.isNotEmpty) { + return field.name.toLowerCase().contains(keyword); + } + + return true; + }); + + return allFields; + } + + void _startListening() { + _onFieldFn = (fields) { + fields.retainWhere((field) => field.canCreateSort); + add(CreateSortEvent.didReceiveFields(fields)); + }; + fieldController.addListener(onReceiveFields: _onFieldFn); + } + + Future> _createDefaultSort(FieldInfo field) async { + final result = await _sortBackendSvc.insertSort( + fieldId: field.id, + fieldType: field.fieldType, + condition: SortConditionPB.Ascending, + ); + + return result; + } + + @override + Future close() async { + if (_onFieldFn != null) { + fieldController.removeListener(onFieldsListener: _onFieldFn); + _onFieldFn = null; + } + return super.close(); + } +} + +@freezed +class CreateSortEvent with _$CreateSortEvent { + const factory CreateSortEvent.initial() = _Initial; + const factory CreateSortEvent.didReceiveFields(List fields) = + _DidReceiveFields; + + const factory CreateSortEvent.createDefaultSort(FieldInfo field) = + _CreateDefaultSort; + + const factory CreateSortEvent.didReceiveFilterText(String text) = + _DidReceiveFilterText; +} + +@freezed +class CreateSortState with _$CreateSortState { + const factory CreateSortState({ + required String filterText, + required List creatableFields, + required List allFields, + required bool didCreateSort, + }) = _CreateSortState; + + factory CreateSortState.initial(List fields) { + return CreateSortState( + filterText: "", + creatableFields: getCreatableSorts(fields), + allFields: fields, + didCreateSort: false, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_editor_bloc.dart new file mode 100644 index 0000000000..bd36dc8dfd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_editor_bloc.dart @@ -0,0 +1,128 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/sort/sort_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbserver.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'util.dart'; + +part 'sort_editor_bloc.freezed.dart'; + +class SortEditorBloc extends Bloc { + final String viewId; + final SortBackendService _sortBackendSvc; + final FieldController fieldController; + void Function(List)? _onFieldFn; + SortEditorBloc({ + required this.viewId, + required this.fieldController, + required List sortInfos, + }) : _sortBackendSvc = SortBackendService(viewId: viewId), + super(SortEditorState.initial(sortInfos, fieldController.fieldInfos)) { + on( + (event, emit) async { + event.when( + initial: () async { + _startListening(); + }, + didReceiveFields: (List fields) { + final List allFields = List.from(fields); + final List creatableFields = List.from(fields); + creatableFields.retainWhere((field) => field.canCreateSort); + emit( + state.copyWith( + allFields: allFields, + creatableFields: creatableFields, + ), + ); + }, + setCondition: (SortInfo sortInfo, SortConditionPB condition) async { + final result = await _sortBackendSvc.updateSort( + fieldId: sortInfo.fieldInfo.id, + sortId: sortInfo.sortId, + fieldType: sortInfo.fieldInfo.fieldType, + condition: condition, + ); + result.fold((l) => {}, (err) => Log.error(err)); + }, + deleteAllSorts: () async { + final result = await _sortBackendSvc.deleteAllSorts(); + result.fold((l) => {}, (err) => Log.error(err)); + }, + didReceiveSorts: (List sortInfos) { + emit(state.copyWith(sortInfos: sortInfos)); + }, + deleteSort: (SortInfo sortInfo) async { + final result = await _sortBackendSvc.deleteSort( + fieldId: sortInfo.fieldInfo.id, + sortId: sortInfo.sortId, + fieldType: sortInfo.fieldInfo.fieldType, + ); + result.fold((l) => null, (err) => Log.error(err)); + }, + ); + }, + ); + } + + void _startListening() { + _onFieldFn = (fields) { + add(SortEditorEvent.didReceiveFields(List.from(fields))); + }; + + fieldController.addListener( + listenWhen: () => !isClosed, + onReceiveFields: _onFieldFn, + onSorts: (sorts) { + add(SortEditorEvent.didReceiveSorts(sorts)); + }, + ); + } + + @override + Future close() async { + if (_onFieldFn != null) { + fieldController.removeListener(onFieldsListener: _onFieldFn); + _onFieldFn = null; + } + return super.close(); + } +} + +@freezed +class SortEditorEvent with _$SortEditorEvent { + const factory SortEditorEvent.initial() = _Initial; + const factory SortEditorEvent.didReceiveFields(List fieldInfos) = + _DidReceiveFields; + const factory SortEditorEvent.didReceiveSorts(List sortInfos) = + _DidReceiveSorts; + const factory SortEditorEvent.setCondition( + SortInfo sortInfo, + SortConditionPB condition, + ) = _SetCondition; + const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort; + const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts; +} + +@freezed +class SortEditorState with _$SortEditorState { + const factory SortEditorState({ + required List sortInfos, + required List creatableFields, + required List allFields, + }) = _SortEditorState; + + factory SortEditorState.initial( + List sortInfos, + List fields, + ) { + return SortEditorState( + creatableFields: getCreatableSorts(fields), + allFields: fields, + sortInfos: sortInfos, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_menu_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_menu_bloc.dart new file mode 100644 index 0000000000..3b039be5b3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_menu_bloc.dart @@ -0,0 +1,115 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import '../../../application/field/field_controller.dart'; +import '../../presentation/widgets/sort/sort_info.dart'; +import 'util.dart'; + +part 'sort_menu_bloc.freezed.dart'; + +class SortMenuBloc extends Bloc { + final String viewId; + final FieldController fieldController; + void Function(List)? _onSortChangeFn; + void Function(List)? _onFieldFn; + + SortMenuBloc({required this.viewId, required this.fieldController}) + : super( + SortMenuState.initial( + viewId, + fieldController.sortInfos, + fieldController.fieldInfos, + ), + ) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveSortInfos: (sortInfos) { + emit(state.copyWith(sortInfos: sortInfos)); + }, + toggleMenu: () { + final isVisible = !state.isVisible; + emit(state.copyWith(isVisible: isVisible)); + }, + didReceiveFields: (List fields) { + emit( + state.copyWith( + fields: fields, + creatableFields: getCreatableSorts(fields), + ), + ); + }, + ); + }, + ); + } + + void _startListening() { + _onSortChangeFn = (sortInfos) { + add(SortMenuEvent.didReceiveSortInfos(sortInfos)); + }; + + _onFieldFn = (fields) { + add(SortMenuEvent.didReceiveFields(fields)); + }; + + fieldController.addListener( + onSorts: (sortInfos) { + _onSortChangeFn?.call(sortInfos); + }, + onReceiveFields: (fields) { + _onFieldFn?.call(fields); + }, + ); + } + + @override + Future close() { + if (_onSortChangeFn != null) { + fieldController.removeListener(onSortsListener: _onSortChangeFn!); + _onSortChangeFn = null; + } + if (_onFieldFn != null) { + fieldController.removeListener(onFieldsListener: _onFieldFn!); + _onFieldFn = null; + } + return super.close(); + } +} + +@freezed +class SortMenuEvent with _$SortMenuEvent { + const factory SortMenuEvent.initial() = _Initial; + const factory SortMenuEvent.didReceiveSortInfos(List sortInfos) = + _DidReceiveSortInfos; + const factory SortMenuEvent.didReceiveFields(List fields) = + _DidReceiveFields; + const factory SortMenuEvent.toggleMenu() = _SetMenuVisibility; +} + +@freezed +class SortMenuState with _$SortMenuState { + const factory SortMenuState({ + required String viewId, + required List sortInfos, + required List fields, + required List creatableFields, + required bool isVisible, + }) = _SortMenuState; + + factory SortMenuState.initial( + String viewId, + List sortInfos, + List fields, + ) => + SortMenuState( + viewId: viewId, + sortInfos: sortInfos, + fields: fields, + creatableFields: getCreatableSorts(fields), + isVisible: false, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/util.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/util.dart new file mode 100644 index 0000000000..1189427515 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/util.dart @@ -0,0 +1,7 @@ +import '../../../application/field/field_controller.dart'; + +List getCreatableSorts(List fieldInfos) { + final List creatableFields = List.from(fieldInfos); + creatableFields.retainWhere((element) => element.canCreateSort); + return creatableFields; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart new file mode 100644 index 0000000000..453f773796 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart @@ -0,0 +1,33 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; + +class GridPluginBuilder implements PluginBuilder { + @override + Plugin build(dynamic data) { + if (data is ViewPB) { + return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); + } else { + throw FlowyPluginException.invalidData; + } + } + + @override + String get menuName => LocaleKeys.grid_menuName.tr(); + + @override + String get menuIcon => "editor/grid"; + + @override + PluginType get pluginType => PluginType.grid; + + @override + ViewLayoutPB? get layoutType => ViewLayoutPB.Grid; +} + +class GridPluginConfig implements PluginConfig { + @override + bool get creatable => true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart new file mode 100755 index 0000000000..cb77073d3c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -0,0 +1,424 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; +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/error_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:linked_scroll_controller/linked_scroll_controller.dart'; +import '../../application/field/field_controller.dart'; +import '../../application/row/row_cache.dart'; +import '../../application/row/row_data_controller.dart'; +import '../application/grid_bloc.dart'; +import '../../application/database_controller.dart'; +import 'grid_scroll.dart'; +import '../../tar_bar/tab_bar_view.dart'; +import 'layout/layout.dart'; +import 'layout/sizes.dart'; +import 'widgets/row/row.dart'; +import 'widgets/footer/grid_footer.dart'; +import 'widgets/header/grid_header.dart'; +import '../../widgets/row/row_detail.dart'; +import 'widgets/shortcuts.dart'; + +class ToggleExtensionNotifier extends ChangeNotifier { + bool _isToggled = false; + + get isToggled => _isToggled; + + void toggle() { + _isToggled = !_isToggled; + notifyListeners(); + } +} + +class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { + final _toggleExtension = ToggleExtensionNotifier(); + + @override + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ) { + return GridPage( + key: _makeValueKey(controller), + view: view, + databaseController: controller, + ); + } + + @override + Widget settingBar(BuildContext context, DatabaseController controller) { + return GridSettingBar( + key: _makeValueKey(controller), + controller: controller, + toggleExtension: _toggleExtension, + ); + } + + @override + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ) { + return DatabaseViewSettingExtension( + key: _makeValueKey(controller), + viewId: controller.viewId, + databaseController: controller, + toggleExtension: _toggleExtension, + ); + } + + ValueKey _makeValueKey(DatabaseController controller) { + return ValueKey(controller.viewId); + } +} + +class GridPage extends StatefulWidget { + final DatabaseController databaseController; + const GridPage({ + required this.view, + required this.databaseController, + this.onDeleted, + Key? key, + }) : super(key: key); + + final ViewPB view; + final VoidCallback? onDeleted; + + @override + State createState() => _GridPageState(); +} + +class _GridPageState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => GridBloc( + view: widget.view, + databaseController: widget.databaseController, + )..add(const GridEvent.initial()), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return state.loadingState.map( + loading: (_) => + const Center(child: CircularProgressIndicator.adaptive()), + finish: (result) => result.successOrFail.fold( + (_) => GridShortcuts( + child: GridPageContent(view: widget.view), + ), + (err) => FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + ), + ); + }, + ), + ); + } +} + +class GridPageContent extends StatefulWidget { + final ViewPB view; + const GridPageContent({ + required this.view, + super.key, + }); + + @override + State createState() => _GridPageContentState(); +} + +class _GridPageContentState extends State { + final _scrollController = GridScrollController( + scrollGroupController: LinkedScrollControllerGroup(), + ); + late final ScrollController headerScrollController; + + @override + void initState() { + super.initState(); + headerScrollController = _scrollController.linkHorizontalController(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.fields != current.fields, + builder: (context, state) { + final contentWidth = GridLayout.headerWidth(state.fields.value); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _GridHeader(headerScrollController: headerScrollController), + _GridRows( + viewId: state.viewId, + contentWidth: contentWidth, + scrollController: _scrollController, + ), + const _GridFooter(), + ], + ); + }, + ); + } +} + +class _GridHeader extends StatelessWidget { + final ScrollController headerScrollController; + const _GridHeader({required this.headerScrollController}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return GridHeaderSliverAdaptor( + viewId: state.viewId, + fieldController: + context.read().databaseController.fieldController, + anchorScrollController: headerScrollController, + ); + }, + ); + } +} + +class _GridRows extends StatelessWidget { + final String viewId; + final double contentWidth; + final GridScrollController scrollController; + + const _GridRows({ + required this.viewId, + required this.contentWidth, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return Flexible( + child: _WrapScrollView( + scrollController: scrollController, + contentWidth: contentWidth, + child: BlocBuilder( + buildWhen: (previous, current) => current.reason.maybeWhen( + reorderRows: () => true, + reorderSingleRow: (reorderRow, rowInfo) => true, + delete: (item) => true, + insert: (item) => true, + orElse: () => false, + ), + builder: (context, state) { + final rowInfos = state.rowInfos; + final behavior = ScrollConfiguration.of(context).copyWith( + scrollbars: false, + ); + return ScrollConfiguration( + behavior: behavior, + child: ReorderableListView.builder( + /// TODO(Xazin): Resolve inconsistent scrollbar behavior + /// This is a workaround related to + /// https://github.com/flutter/flutter/issues/25652 + cacheExtent: 5000, + scrollController: scrollController.verticalController, + buildDefaultDragHandles: false, + proxyDecorator: (child, index, animation) => Material( + color: Colors.white.withOpacity(.1), + child: Opacity(opacity: .5, child: child), + ), + onReorder: (fromIndex, newIndex) { + final toIndex = + newIndex > fromIndex ? newIndex - 1 : newIndex; + if (fromIndex == toIndex) { + return; + } + context + .read() + .add(GridEvent.moveRow(fromIndex, toIndex)); + }, + itemCount: rowInfos.length + 1, // the extra item is the footer + itemBuilder: (context, index) { + if (index < rowInfos.length) { + final rowInfo = rowInfos[index]; + return _renderRow( + context, + rowInfo.rowId, + isDraggable: state.reorderable, + index: index, + ); + } + return const GridRowBottomBar(key: Key('gridFooter')); + }, + ), + ); + }, + ), + ), + ); + } + + Widget _renderRow( + BuildContext context, + RowId rowId, { + int? index, + required bool isDraggable, + Animation? animation, + }) { + final rowCache = context.read().getRowCache(rowId); + final rowMeta = rowCache.getRow(rowId)?.rowMeta; + + /// Return placeholder widget if the rowMeta is null. + if (rowMeta == null) return const SizedBox.shrink(); + + final fieldController = + context.read().databaseController.fieldController; + final dataController = RowController( + viewId: viewId, + rowMeta: rowMeta, + rowCache: rowCache, + ); + + final child = GridRow( + key: ValueKey(rowMeta.id), + rowId: rowId, + viewId: viewId, + index: index, + isDraggable: isDraggable, + dataController: dataController, + cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + openDetailPage: (context, cellBuilder) { + _openRowDetailPage( + context, + rowId, + fieldController, + rowCache, + cellBuilder, + ); + }, + ); + + if (animation != null) { + return SizeTransition( + sizeFactor: animation, + child: child, + ); + } + + return child; + } + + void _openRowDetailPage( + BuildContext context, + RowId rowId, + FieldController fieldController, + RowCache rowCache, + GridCellBuilder cellBuilder, + ) { + final rowMeta = rowCache.getRow(rowId)?.rowMeta; + // Most of the cases, the rowMeta should not be null. + if (rowMeta != null) { + final dataController = RowController( + viewId: viewId, + rowMeta: rowMeta, + rowCache: rowCache, + ); + + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: cellBuilder, + rowController: dataController, + ); + }, + ); + } else { + Log.warn('RowMeta is null for rowId: $rowId'); + } + } +} + +class _WrapScrollView extends StatelessWidget { + const _WrapScrollView({ + required this.contentWidth, + required this.scrollController, + required this.child, + }); + + final GridScrollController scrollController; + final double contentWidth; + final Widget child; + + @override + Widget build(BuildContext context) { + return ScrollbarListStack( + axis: Axis.vertical, + controller: scrollController.verticalController, + barSize: GridSize.scrollBarSize, + autoHideScrollbar: false, + child: StyledSingleChildScrollView( + controller: scrollController.horizontalController, + axis: Axis.horizontal, + child: SizedBox( + width: contentWidth, + child: child, + ), + ), + ); + } +} + +class _GridFooter extends StatelessWidget { + const _GridFooter(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.rowCount, + builder: (context, rowCount) { + return Padding( + padding: GridSize.contentInsets, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + FlowyText.medium( + rowCountString(rowCount), + color: Theme.of(context).hintColor, + ), + ], + ), + ); + }, + ); + } +} + +String rowCountString(int count) { + return '${LocaleKeys.grid_row_count.tr()} : $count'; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_scroll.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_scroll.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_scroll.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_scroll.dart index 866a9d11b5..90e29d6716 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_scroll.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_scroll.dart @@ -2,18 +2,18 @@ import 'package:flutter/material.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; class GridScrollController { - GridScrollController({ - required LinkedScrollControllerGroup scrollGroupController, - }) : _scrollGroupController = scrollGroupController, - verticalController = ScrollController(), - horizontalController = scrollGroupController.addAndGet(); - final LinkedScrollControllerGroup _scrollGroupController; final ScrollController verticalController; final ScrollController horizontalController; final List _linkHorizontalControllers = []; + GridScrollController({ + required LinkedScrollControllerGroup scrollGroupController, + }) : _scrollGroupController = scrollGroupController, + verticalController = ScrollController(), + horizontalController = scrollGroupController.addAndGet(); + ScrollController linkHorizontalController() { final controller = _scrollGroupController.addAndGet(); _linkHorizontalControllers.add(controller); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/layout.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/layout.dart new file mode 100755 index 0000000000..1cb17c8ebe --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/layout.dart @@ -0,0 +1,16 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'sizes.dart'; + +class GridLayout { + static double headerWidth(List fields) { + if (fields.isEmpty) return 0; + + final fieldsWidth = fields + .map((field) => field.width.toDouble()) + .reduce((value, element) => value + element); + + return fieldsWidth + + GridSize.leadingHeaderPadding + + GridSize.trailHeaderPadding; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart new file mode 100755 index 0000000000..b6292e0d2f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart @@ -0,0 +1,46 @@ +import 'package:flutter/widgets.dart'; + +class GridSize { + static double scale = 1; + + static double get scrollBarSize => 12 * scale; + static double get headerHeight => 40 * scale; + static double get footerHeight => 40 * scale; + static double get leadingHeaderPadding => 50 * scale; + static double get trailHeaderPadding => 140 * scale; + static double get headerContainerPadding => 0 * scale; + static double get cellHPadding => 10 * scale; + static double get cellVPadding => 10 * scale; + static double get popoverItemHeight => 32 * scale; + static double get typeOptionSeparatorHeight => 4 * scale; + + static EdgeInsets get headerContentInsets => EdgeInsets.symmetric( + horizontal: GridSize.headerContainerPadding, + vertical: GridSize.headerContainerPadding, + ); + static EdgeInsets get cellContentInsets => EdgeInsets.symmetric( + horizontal: GridSize.cellHPadding, + vertical: GridSize.cellVPadding, + ); + + static EdgeInsets get fieldContentInsets => EdgeInsets.symmetric( + horizontal: GridSize.cellHPadding, + vertical: GridSize.cellVPadding, + ); + + static EdgeInsets get typeOptionContentInsets => const EdgeInsets.all(4); + + static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB( + GridSize.leadingHeaderPadding, + GridSize.headerContainerPadding, + GridSize.headerContainerPadding, + GridSize.headerContainerPadding, + ); + + static EdgeInsets get contentInsets => EdgeInsets.fromLTRB( + GridSize.leadingHeaderPadding, + GridSize.headerContainerPadding, + GridSize.leadingHeaderPadding, + GridSize.headerContainerPadding, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/common/type_option_separator.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart similarity index 81% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/common/type_option_separator.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart index 1d316ca5cb..8539516d65 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/common/type_option_separator.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; class TypeOptionSeparator extends StatelessWidget { - const TypeOptionSeparator({this.spacing = 6.0, super.key}); - final double spacing; + const TypeOptionSeparator({this.spacing = 6.0, Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart new file mode 100644 index 0000000000..4bbb535762 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart @@ -0,0 +1,210 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/application/filter/checkbox_filter_editor_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/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../condition_button.dart'; +import '../disclosure_button.dart'; +import '../filter_info.dart'; +import 'choicechip.dart'; + +class CheckboxFilterChoicechip extends StatefulWidget { + final FilterInfo filterInfo; + const CheckboxFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @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 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 { + final CheckboxFilterEditorBloc bloc; + const CheckboxFilterEditor({required this.bloc, Key? key}) : super(key: key); + + @override + State createState() => _CheckboxFilterEditorState(); +} + +class _CheckboxFilterEditorState extends State { + final popoverMutex = PopoverMutex(); + + @override + 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)), + ); + }, + ), + ); + } + + Widget _buildFilterPanel( + BuildContext context, + CheckboxFilterEditorState state, + ) { + return SizedBox( + height: 20, + child: Row( + children: [ + FlowyText(state.filterInfo.fieldInfo.name), + const HSpace(4), + CheckboxFilterConditionList( + filterInfo: state.filterInfo, + popoverMutex: popoverMutex, + onCondition: (condition) { + context + .read() + .add(CheckboxFilterEditorEvent.updateCondition(condition)); + }, + ), + const Spacer(), + DisclosureButton( + popoverMutex: popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const CheckboxFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + } +} + +class CheckboxFilterConditionList extends StatelessWidget { + final FilterInfo filterInfo; + final PopoverMutex popoverMutex; + final Function(CheckboxFilterConditionPB) onCondition; + const CheckboxFilterConditionList({ + required this.filterInfo, + required this.popoverMutex, + required this.onCondition, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final checkboxFilter = filterInfo.checkboxFilter()!; + return PopoverActionList( + asBarrier: true, + mutex: popoverMutex, + direction: PopoverDirection.bottomWithCenterAligned, + actions: CheckboxFilterConditionPB.values + .map( + (action) => ConditionWrapper( + action, + checkboxFilter.condition == action, + ), + ) + .toList(), + buildChild: (controller) { + return ConditionButton( + conditionName: checkboxFilter.condition.filterName, + onTap: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + onCondition(action.inner); + controller.close(); + }, + ); + } +} + +class ConditionWrapper extends ActionCell { + final CheckboxFilterConditionPB inner; + final bool isSelected; + + ConditionWrapper(this.inner, this.isSelected); + + @override + Widget? rightIcon(Color iconColor) { + if (isSelected) { + return svgWidget("grid/checkmark"); + } else { + return null; + } + } + + @override + String get name => inner.filterName; +} + +extension TextFilterConditionPBExtension on CheckboxFilterConditionPB { + String get filterName { + switch (this) { + case CheckboxFilterConditionPB.IsChecked: + return LocaleKeys.grid_checkboxFilter_isChecked.tr(); + case CheckboxFilterConditionPB.IsUnChecked: + return LocaleKeys.grid_checkboxFilter_isUnchecked.tr(); + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart new file mode 100644 index 0000000000..fef7c0276d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart @@ -0,0 +1,173 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/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 { + final FilterInfo filterInfo; + const ChecklistFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @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 { + final ChecklistFilterEditorBloc bloc; + final PopoverMutex popoverMutex; + const ChecklistFilterEditor({ + required this.bloc, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @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: [ + FlowyText(state.filterInfo.fieldInfo.name), + const HSpace(4), + ChecklistFilterConditionList( + filterInfo: state.filterInfo, + ), + const Spacer(), + DisclosureButton( + popoverMutex: widget.popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const ChecklistFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + }, + ), + ); + } +} + +class ChecklistFilterConditionList extends StatelessWidget { + final FilterInfo filterInfo; + const ChecklistFilterConditionList({ + required this.filterInfo, + Key? key, + }) : super(key: key); + + @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 { + final ChecklistFilterConditionPB inner; + + ConditionWrapper(this.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_view/grid/presentation/widgets/filter/choicechip/choicechip.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/choicechip.dart new file mode 100644 index 0000000000..144839220b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/choicechip.dart @@ -0,0 +1,83 @@ +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/image.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 'dart:math' as math; + +import '../filter_info.dart'; + +class ChoiceChipButton extends StatelessWidget { + final FilterInfo filterInfo; + final VoidCallback? onTap; + final String filterDesc; + + const ChoiceChipButton({ + Key? key, + required this.filterInfo, + this.filterDesc = '', + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final borderSide = BorderSide( + color: AFThemeExtension.of(context).toggleOffFill, + width: 1.0, + ); + + final decoration = BoxDecoration( + color: Colors.transparent, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(14)), + ); + + return SizedBox( + height: 28, + child: FlowyButton( + decoration: decoration, + useIntrinsicWidth: true, + text: FlowyText( + filterInfo.fieldInfo.name, + color: AFThemeExtension.of(context).textColor, + ), + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + radius: const BorderRadius.all(Radius.circular(14)), + leftIcon: svgWidget( + filterInfo.fieldInfo.fieldType.iconName(), + color: Theme.of(context).iconTheme.color, + ), + rightIcon: _ChoicechipFilterDesc(filterDesc: filterDesc), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: onTap, + ), + ); + } +} + +class _ChoicechipFilterDesc extends StatelessWidget { + final String filterDesc; + const _ChoicechipFilterDesc({this.filterDesc = '', Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final arrow = Transform.rotate( + angle: -math.pi / 2, + child: svgWidget( + "home/arrow_left", + color: AFThemeExtension.of(context).textColor, + ), + ); + 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_view/grid/presentation/widgets/filter/choicechip/date.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/date.dart new file mode 100644 index 0000000000..f63ed27e47 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/date.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +import '../filter_info.dart'; +import 'choicechip.dart'; + +class DateFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const DateFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/number.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/number.dart new file mode 100644 index 0000000000..ee3c88bd70 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/number.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +import '../filter_info.dart'; +import 'choicechip.dart'; + +class NumberFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const NumberFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart new file mode 100644 index 0000000000..e3151f51d9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart @@ -0,0 +1,118 @@ +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:flowy_infra/image.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:flutter/material.dart'; + +import '../../condition_button.dart'; +import '../../filter_info.dart'; + +class SelectOptionFilterConditionList extends StatelessWidget { + final FilterInfo filterInfo; + final PopoverMutex popoverMutex; + final Function(SelectOptionConditionPB) onCondition; + const SelectOptionFilterConditionList({ + required this.filterInfo, + required this.popoverMutex, + required this.onCondition, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final selectOptionFilter = filterInfo.selectOptionFilter()!; + return PopoverActionList( + asBarrier: true, + mutex: popoverMutex, + direction: PopoverDirection.bottomWithCenterAligned, + actions: SelectOptionConditionPB.values + .map( + (action) => ConditionWrapper( + action, + selectOptionFilter.condition == action, + filterInfo.fieldInfo.fieldType, + ), + ) + .toList(), + buildChild: (controller) { + return ConditionButton( + conditionName: filterName(selectOptionFilter), + onTap: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + onCondition(action.inner); + controller.close(); + }, + ); + } + + String filterName(SelectOptionFilterPB filter) { + if (filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) { + return filter.condition.singleSelectFilterName; + } else { + return filter.condition.multiSelectFilterName; + } + } +} + +class ConditionWrapper extends ActionCell { + final SelectOptionConditionPB inner; + final bool isSelected; + final FieldType fieldType; + + ConditionWrapper(this.inner, this.isSelected, this.fieldType); + + @override + Widget? rightIcon(Color iconColor) { + if (isSelected) { + return svgWidget("grid/checkmark"); + } else { + return null; + } + } + + @override + String get name { + if (fieldType == FieldType.SingleSelect) { + return inner.singleSelectFilterName; + } else { + return inner.multiSelectFilterName; + } + } +} + +extension SelectOptionConditionPBExtension on SelectOptionConditionPB { + String get singleSelectFilterName { + switch (this) { + case SelectOptionConditionPB.OptionIs: + return LocaleKeys.grid_singleSelectOptionFilter_is.tr(); + case SelectOptionConditionPB.OptionIsEmpty: + return LocaleKeys.grid_singleSelectOptionFilter_isEmpty.tr(); + case SelectOptionConditionPB.OptionIsNot: + return LocaleKeys.grid_singleSelectOptionFilter_isNot.tr(); + case SelectOptionConditionPB.OptionIsNotEmpty: + return LocaleKeys.grid_singleSelectOptionFilter_isNotEmpty.tr(); + default: + return ""; + } + } + + String get multiSelectFilterName { + switch (this) { + case SelectOptionConditionPB.OptionIs: + return LocaleKeys.grid_multiSelectOptionFilter_contains.tr(); + case SelectOptionConditionPB.OptionIsEmpty: + return LocaleKeys.grid_multiSelectOptionFilter_isEmpty.tr(); + case SelectOptionConditionPB.OptionIsNot: + return LocaleKeys.grid_multiSelectOptionFilter_doesNotContain.tr(); + case SelectOptionConditionPB.OptionIsNotEmpty: + return LocaleKeys.grid_multiSelectOptionFilter_isNotEmpty.tr(); + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart new file mode 100644 index 0000000000..c4a344f683 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart @@ -0,0 +1,123 @@ +import 'package:appflowy/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../../widgets/row/cells/select_option_cell/extension.dart'; +import '../../filter_info.dart'; +import 'select_option_loader.dart'; + +class SelectOptionFilterList extends StatelessWidget { + final FilterInfo filterInfo; + final List selectedOptionIds; + final Function(List) onSelectedOptions; + const SelectOptionFilterList({ + required this.filterInfo, + required this.selectedOptionIds, + required this.onSelectedOptions, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + late SelectOptionFilterListBloc bloc; + if (filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) { + bloc = SelectOptionFilterListBloc( + viewId: filterInfo.viewId, + fieldPB: filterInfo.fieldInfo.field, + selectedOptionIds: selectedOptionIds, + delegate: SingleSelectOptionFilterDelegateImpl(filterInfo), + ); + } else { + bloc = SelectOptionFilterListBloc( + viewId: filterInfo.viewId, + fieldPB: filterInfo.fieldInfo.field, + selectedOptionIds: selectedOptionIds, + delegate: MultiSelectOptionFilterDelegateImpl(filterInfo), + ); + } + + bloc.add(const SelectOptionFilterListEvent.initial()); + return bloc; + }, + child: + BlocListener( + listenWhen: (previous, current) => + previous.selectedOptionIds != current.selectedOptionIds, + listener: (context, state) { + onSelectedOptions(state.selectedOptionIds.toList()); + }, + child: BlocBuilder( + builder: (context, state) { + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + controller: ScrollController(), + 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 { + final SelectOptionPB option; + final bool isSelected; + const SelectOptionFilterCell({ + required this.option, + required this.isSelected, + Key? key, + }) : super(key: key); + + @override + State createState() => _SelectOptionFilterCellState(); +} + +class _SelectOptionFilterCellState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: SelectOptionTagCell( + option: widget.option, + onSelected: (option) { + if (widget.isSelected) { + context + .read() + .add(SelectOptionFilterListEvent.unselectOption(option)); + } else { + context + .read() + .add(SelectOptionFilterListEvent.selectOption(option)); + } + }, + children: [ + if (widget.isSelected) + Padding( + padding: const EdgeInsets.only(right: 6), + child: svgWidget("grid/checkmark"), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart new file mode 100644 index 0000000000..334086adfe --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart @@ -0,0 +1,173 @@ +import 'package:appflowy/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.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: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 StatefulWidget { + final FilterInfo filterInfo; + const SelectOptionFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @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(widget.filterInfo), + ); + } else { + bloc = SelectOptionFilterEditorBloc( + filterInfo: widget.filterInfo, + delegate: MultiSelectOptionFilterDelegateImpl(widget.filterInfo), + ); + } + bloc.add(const SelectOptionFilterEditorEvent.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(240, 160)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (BuildContext context) { + return SelectOptionFilterEditor(bloc: bloc); + }, + child: ChoiceChipButton( + filterInfo: widget.filterInfo, + filterDesc: state.filterDesc, + ), + ); + }, + ), + ); + } +} + +class SelectOptionFilterEditor extends StatefulWidget { + final SelectOptionFilterEditorBloc bloc; + const SelectOptionFilterEditor({required this.bloc, Key? key}) + : super(key: key); + + @override + State createState() => + _SelectOptionFilterEditorState(); +} + +class _SelectOptionFilterEditorState extends State { + final popoverMutex = PopoverMutex(); + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: widget.bloc, + child: BlocBuilder( + builder: (context, state) { + final List slivers = [ + SliverToBoxAdapter(child: _buildFilterPanel(context, state)), + ]; + + if (state.filter.condition != SelectOptionConditionPB.OptionIsEmpty && + state.filter.condition != + SelectOptionConditionPB.OptionIsNotEmpty) { + slivers.add(const SliverToBoxAdapter(child: VSpace(4))); + slivers.add( + SliverToBoxAdapter( + child: SelectOptionFilterList( + 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, + controller: ScrollController(), + physics: StyledScrollPhysics(), + ), + ); + }, + ), + ); + } + + Widget _buildFilterPanel( + BuildContext context, + SelectOptionFilterEditorState state, + ) { + return SizedBox( + height: 20, + child: Row( + children: [ + FlowyText(state.filterInfo.fieldInfo.name), + const HSpace(4), + SelectOptionFilterConditionList( + filterInfo: state.filterInfo, + popoverMutex: popoverMutex, + onCondition: (condition) { + context.read().add( + SelectOptionFilterEditorEvent.updateCondition(condition), + ); + }, + ), + const Spacer(), + DisclosureButton( + popoverMutex: popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const SelectOptionFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart new file mode 100644 index 0000000000..e4e5a27c88 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart @@ -0,0 +1,50 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; + +import '../../filter_info.dart'; + +abstract class SelectOptionFilterDelegate { + Future> loadOptions(); +} + +class SingleSelectOptionFilterDelegateImpl + implements SelectOptionFilterDelegate { + final SingleSelectTypeOptionContext typeOptionContext; + + SingleSelectOptionFilterDelegateImpl(FilterInfo filterInfo) + : typeOptionContext = makeSingleSelectTypeOptionContext( + viewId: filterInfo.viewId, + fieldPB: filterInfo.fieldInfo.field, + ); + + @override + Future> loadOptions() { + return typeOptionContext + .loadTypeOptionData( + onError: (error) => Log.error(error), + ) + .then((value) => value.options); + } +} + +class MultiSelectOptionFilterDelegateImpl + implements SelectOptionFilterDelegate { + final MultiSelectTypeOptionContext typeOptionContext; + + MultiSelectOptionFilterDelegateImpl(FilterInfo filterInfo) + : typeOptionContext = makeMultiSelectTypeOptionContext( + viewId: filterInfo.viewId, + fieldPB: filterInfo.fieldInfo.field, + ); + + @override + Future> loadOptions() { + return typeOptionContext + .loadTypeOptionData( + onError: (error) => Log.error(error), + ) + .then((value) => value.options); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart new file mode 100644 index 0000000000..f4b426ca9f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart @@ -0,0 +1,268 @@ +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:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../application/filter/text_filter_editor_bloc.dart'; +import '../condition_button.dart'; +import '../disclosure_button.dart'; +import '../filter_info.dart'; +import 'choicechip.dart'; + +class TextFilterChoicechip extends StatefulWidget { + final FilterInfo filterInfo; + const TextFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + State createState() => _TextFilterChoicechipState(); +} + +class _TextFilterChoicechipState extends State { + late TextFilterEditorBloc bloc; + + @override + void initState() { + bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo) + ..add(const TextFilterEditorEvent.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, 76)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (BuildContext context) { + return TextFilterEditor(bloc: bloc); + }, + child: ChoiceChipButton( + filterInfo: widget.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 { + final TextFilterEditorBloc bloc; + const TextFilterEditor({required this.bloc, Key? key}) : super(key: key); + + @override + State createState() => _TextFilterEditorState(); +} + +class _TextFilterEditorState extends State { + final popoverMutex = PopoverMutex(); + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: widget.bloc, + child: BlocBuilder( + builder: (context, state) { + final List children = [ + _buildFilterPanel(context, state), + ]; + + if (state.filter.condition != TextFilterConditionPB.TextIsEmpty && + state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) { + children.add(const VSpace(4)); + children.add(_buildFilterTextField(context, state)); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: IntrinsicHeight(child: Column(children: children)), + ); + }, + ), + ); + } + + Widget _buildFilterPanel(BuildContext context, TextFilterEditorState state) { + return SizedBox( + height: 20, + child: Row( + children: [ + FlowyText(state.filterInfo.fieldInfo.name), + const HSpace(4), + TextFilterConditionPBList( + filterInfo: state.filterInfo, + popoverMutex: popoverMutex, + onCondition: (condition) { + context + .read() + .add(TextFilterEditorEvent.updateCondition(condition)); + }, + ), + const Spacer(), + DisclosureButton( + popoverMutex: popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const TextFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + } + + Widget _buildFilterTextField( + BuildContext context, + TextFilterEditorState state, + ) { + return FlowyTextField( + text: state.filter.content, + hintText: LocaleKeys.grid_settings_typeAValue.tr(), + debounceDuration: const Duration(milliseconds: 300), + autoFocus: false, + onChanged: (text) { + context + .read() + .add(TextFilterEditorEvent.updateContent(text)); + }, + ); + } +} + +class TextFilterConditionPBList extends StatelessWidget { + final FilterInfo filterInfo; + final PopoverMutex popoverMutex; + final Function(TextFilterConditionPB) onCondition; + const TextFilterConditionPBList({ + required this.filterInfo, + required this.popoverMutex, + required this.onCondition, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final textFilter = filterInfo.textFilter()!; + return PopoverActionList( + asBarrier: true, + mutex: popoverMutex, + direction: PopoverDirection.bottomWithCenterAligned, + actions: TextFilterConditionPB.values + .map( + (action) => ConditionWrapper( + action, + textFilter.condition == action, + ), + ) + .toList(), + buildChild: (controller) { + return ConditionButton( + conditionName: textFilter.condition.filterName, + onTap: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + onCondition(action.inner); + controller.close(); + }, + ); + } +} + +class ConditionWrapper extends ActionCell { + final TextFilterConditionPB inner; + final bool isSelected; + + ConditionWrapper(this.inner, this.isSelected); + + @override + Widget? rightIcon(Color iconColor) { + if (isSelected) { + return svgWidget("grid/checkmark"); + } else { + return null; + } + } + + @override + String get name => inner.filterName; +} + +extension TextFilterConditionPBExtension on TextFilterConditionPB { + String get filterName { + switch (this) { + case TextFilterConditionPB.Contains: + return LocaleKeys.grid_textFilter_contains.tr(); + case TextFilterConditionPB.DoesNotContain: + return LocaleKeys.grid_textFilter_doesNotContain.tr(); + case TextFilterConditionPB.EndsWith: + return LocaleKeys.grid_textFilter_endsWith.tr(); + case TextFilterConditionPB.Is: + return LocaleKeys.grid_textFilter_is.tr(); + case TextFilterConditionPB.IsNot: + return LocaleKeys.grid_textFilter_isNot.tr(); + case TextFilterConditionPB.StartsWith: + return LocaleKeys.grid_textFilter_startWith.tr(); + case TextFilterConditionPB.TextIsEmpty: + return LocaleKeys.grid_textFilter_isEmpty.tr(); + case TextFilterConditionPB.TextIsNotEmpty: + return LocaleKeys.grid_textFilter_isNotEmpty.tr(); + default: + return ""; + } + } + + String get choicechipPrefix { + switch (this) { + case TextFilterConditionPB.DoesNotContain: + return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr(); + case TextFilterConditionPB.EndsWith: + return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr(); + case TextFilterConditionPB.IsNot: + return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr(); + case TextFilterConditionPB.StartsWith: + return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr(); + case TextFilterConditionPB.TextIsEmpty: + return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr(); + case TextFilterConditionPB.TextIsNotEmpty: + return LocaleKeys.grid_textFilter_choicechipPrefix_isNotEmpty.tr(); + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/url.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/url.dart new file mode 100644 index 0000000000..72bad7ee95 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/url.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import '../filter_info.dart'; +import 'choicechip.dart'; + +class URLFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const URLFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/condition_button.dart similarity index 79% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/condition_button.dart index 2be7810546..58c625348f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/condition_button.dart @@ -1,28 +1,25 @@ 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/image.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 ConditionButton extends StatelessWidget { - const ConditionButton({ - super.key, - required this.conditionName, - required this.onTap, - }); - final String conditionName; final VoidCallback onTap; + const ConditionButton({ + required this.conditionName, + required this.onTap, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { final arrow = Transform.rotate( angle: -math.pi / 2, - child: FlowySvg( - FlowySvgs.arrow_left_s, + child: svgWidget( + "home/arrow_left", color: AFThemeExtension.of(context).textColor, ), ); @@ -32,14 +29,12 @@ 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_view/grid/presentation/widgets/filter/create_filter_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/create_filter_list.dart new file mode 100644 index 0000000000..e9c377b5f7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/create_filter_list.dart @@ -0,0 +1,174 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../application/field/field_controller.dart'; +import '../../../application/filter/filter_create_bloc.dart'; + +class GridCreateFilterList extends StatefulWidget { + final String viewId; + final FieldController fieldController; + final VoidCallback onClosed; + final VoidCallback? onCreateFilter; + + const GridCreateFilterList({ + required this.viewId, + required this.fieldController, + required this.onClosed, + this.onCreateFilter, + Key? key, + }) : super(key: key); + + @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) { + return BlocProvider.value( + value: editBloc, + child: BlocListener( + listener: (context, state) { + if (state.didCreateFilter) { + widget.onClosed(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cells = state.creatableFields.map((fieldInfo) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: GridFilterPropertyCell( + fieldInfo: fieldInfo, + onTap: (fieldInfo) => createFilter(fieldInfo), + ), + ); + }).toList(); + + final List slivers = [ + SliverPersistentHeader( + pinned: true, + delegate: _FilterTextFieldDelegate(), + ), + SliverToBoxAdapter( + child: ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + separatorBuilder: (BuildContext context, int index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + ), + ), + ]; + return CustomScrollView( + shrinkWrap: true, + slivers: slivers, + controller: ScrollController(), + physics: StyledScrollPhysics(), + ); + }, + ), + ), + ); + } + + @override + Future dispose() async { + editBloc.close(); + super.dispose(); + } + + void createFilter(FieldInfo field) { + editBloc.add(GridCreateFilterEvent.createDefaultFilter(field)); + widget.onCreateFilter?.call(); + } +} + +class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate { + _FilterTextFieldDelegate(); + + double fixHeight = 46; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Container( + padding: const EdgeInsets.only(top: 4), + height: fixHeight, + child: FlowyTextField( + hintText: LocaleKeys.grid_settings_filterBy.tr(), + onChanged: (text) { + context + .read() + .add(GridCreateFilterEvent.didReceiveFilterText(text)); + }, + ), + ); + } + + @override + double get maxExtent => fixHeight; + + @override + double get minExtent => fixHeight; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return false; + } +} + +class GridFilterPropertyCell extends StatelessWidget { + final FieldInfo fieldInfo; + final Function(FieldInfo) onTap; + const GridFilterPropertyCell({ + required this.fieldInfo, + required this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + fieldInfo.name, + color: AFThemeExtension.of(context).textColor, + ), + onTap: () => onTap(fieldInfo), + leftIcon: svgWidget( + fieldInfo.fieldType.iconName(), + color: Theme.of(context).iconTheme.color, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart similarity index 91% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart index 4f71e48ab3..fd296a73e0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart @@ -1,22 +1,20 @@ -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_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; - +import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; class DisclosureButton extends StatefulWidget { - const DisclosureButton({ - super.key, - required this.popoverMutex, - required this.onAction, - }); - final PopoverMutex popoverMutex; final Function(FilterDisclosureAction) onAction; + const DisclosureButton({ + required this.popoverMutex, + required this.onAction, + Key? key, + }) : super(key: key); @override State createState() => _DisclosureButtonState(); @@ -28,6 +26,7 @@ class _DisclosureButtonState extends State { return PopoverActionList( asBarrier: true, mutex: widget.popoverMutex, + direction: PopoverDirection.rightWithTopAligned, actions: FilterDisclosureAction.values .map((action) => FilterDisclosureActionWrapper(action)) .toList(), @@ -35,8 +34,8 @@ class _DisclosureButtonState extends State { return FlowyIconButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 20, - icon: FlowySvg( - FlowySvgs.details_s, + icon: svgWidget( + "editor/details", color: Theme.of(context).iconTheme.color, ), onPressed: () => controller.show(), @@ -55,10 +54,10 @@ enum FilterDisclosureAction { } class FilterDisclosureActionWrapper extends ActionCell { - FilterDisclosureActionWrapper(this.inner); - final FilterDisclosureAction inner; + FilterDisclosureActionWrapper(this.inner); + @override Widget? leftIcon(Color iconColor) => null; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart new file mode 100644 index 0000000000..f8a6091cbf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart @@ -0,0 +1,66 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; + +class FilterInfo { + final String viewId; + final FilterPB filter; + final FieldInfo fieldInfo; + + FilterInfo(this.viewId, this.filter, this.fieldInfo); + + FilterInfo copyWith({FilterPB? filter, FieldInfo? fieldInfo}) { + return FilterInfo( + viewId, + filter ?? this.filter, + fieldInfo ?? this.fieldInfo, + ); + } + + DateFilterPB? dateFilter() { + if (![ + FieldType.DateTime, + FieldType.LastEditedTime, + FieldType.CreatedTime, + ].contains(filter.fieldType)) { + return null; + } + return DateFilterPB.fromBuffer(filter.data); + } + + TextFilterPB? textFilter() { + if (filter.fieldType != FieldType.RichText) { + return null; + } + return TextFilterPB.fromBuffer(filter.data); + } + + CheckboxFilterPB? checkboxFilter() { + if (filter.fieldType != FieldType.Checkbox) { + return null; + } + return CheckboxFilterPB.fromBuffer(filter.data); + } + + SelectOptionFilterPB? selectOptionFilter() { + if (filter.fieldType == FieldType.SingleSelect || + filter.fieldType == FieldType.MultiSelect) { + return SelectOptionFilterPB.fromBuffer(filter.data); + } else { + return null; + } + } + + ChecklistFilterPB? checklistFilter() { + if (filter.fieldType == FieldType.Checklist) { + return ChecklistFilterPB.fromBuffer(filter.data); + } else { + return null; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart new file mode 100644 index 0000000000..8df5fc9add --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart @@ -0,0 +1,120 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/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/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'create_filter_list.dart'; +import 'filter_menu_item.dart'; + +class FilterMenu extends StatelessWidget { + final FieldController fieldController; + const FilterMenu({ + required this.fieldController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => GridFilterMenuBloc( + viewId: fieldController.viewId, + fieldController: fieldController, + )..add( + const GridFilterMenuEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + final List children = []; + children.addAll( + state.filters + .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo)) + .toList(), + ); + + if (state.creatableFields.isNotEmpty) { + children.add(AddFilterButton(viewId: state.viewId)); + } + + return Expanded( + child: Row( + children: [ + Expanded( + child: Wrap( + spacing: 6, + runSpacing: 4, + children: children, + ), + ), + ], + ), + ); + }, + ), + ); + } +} + +class AddFilterButton extends StatefulWidget { + final String viewId; + const AddFilterButton({required this.viewId, Key? key}) : super(key: key); + + @override + State createState() => _AddFilterButtonState(); +} + +class _AddFilterButtonState extends State { + 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( + LocaleKeys.grid_settings_addFilter.tr(), + color: AFThemeExtension.of(context).textColor, + ), + useIntrinsicWidth: true, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + leftIcon: svgWidget( + "home/add", + color: Theme.of(context).iconTheme.color, + ), + onTap: () => popoverController.show(), + ), + ), + ); + } + + Widget wrapPopover(BuildContext buildContext, Widget child) { + return AppFlowyPopover( + controller: popoverController, + constraints: BoxConstraints.loose(const Size(200, 300)), + margin: const EdgeInsets.all(6), + triggerActions: PopoverTriggerFlags.none, + child: child, + 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_view/grid/presentation/widgets/filter/filter_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart new file mode 100644 index 0000000000..d2e13b8b53 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart @@ -0,0 +1,46 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:flutter/material.dart'; + +import 'choicechip/checkbox.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 { + final FilterInfo filterInfo; + const FilterMenuItem({required this.filterInfo, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return buildFilterChoicechip(filterInfo); + } +} + +Widget buildFilterChoicechip(FilterInfo filterInfo) { + switch (filterInfo.fieldInfo.fieldType) { + case FieldType.Checkbox: + return CheckboxFilterChoicechip(filterInfo: filterInfo); + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateFilterChoicechip(filterInfo: filterInfo); + case FieldType.MultiSelect: + return SelectOptionFilterChoicechip(filterInfo: filterInfo); + case FieldType.Number: + return NumberFilterChoicechip(filterInfo: filterInfo); + case FieldType.RichText: + return TextFilterChoicechip(filterInfo: filterInfo); + case FieldType.SingleSelect: + return SelectOptionFilterChoicechip(filterInfo: filterInfo); + case FieldType.URL: + return URLFilterChoicechip(filterInfo: filterInfo); + case FieldType.Checklist: + return ChecklistFilterChoicechip(filterInfo: filterInfo); + default: + return const SizedBox(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart new file mode 100755 index 0000000000..62f250b43b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart @@ -0,0 +1,46 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/image.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 { + const GridAddRowButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_row_newRow.tr(), + color: Theme.of(context).colorScheme.tertiary, + ), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: () => context.read().add(const GridEvent.createRow()), + leftIcon: svgWidget( + "home/add", + color: Theme.of(context).colorScheme.tertiary, + ), + ); + } +} + +class GridRowBottomBar extends StatelessWidget { + const GridRowBottomBar({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: GridSize.footerContentInsets, + height: GridSize.footerHeight, + margin: const EdgeInsets.only(bottom: 200), + child: const GridAddRowButton(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/constants.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/constants.dart new file mode 100755 index 0000000000..0a02de5076 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/constants.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +class GridHeaderConstants { + static Color get backgroundColor => Colors.grey; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart new file mode 100755 index 0000000000..d2562cdb84 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart @@ -0,0 +1,186 @@ +import 'package:appflowy/plugins/database_view/application/field/field_cell_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.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:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../layout/sizes.dart'; +import 'field_cell_action_sheet.dart'; +import 'field_type_extension.dart'; + +class GridFieldCell extends StatefulWidget { + final FieldContext cellContext; + const GridFieldCell({ + Key? key, + required this.cellContext, + }) : super(key: key); + + @override + State createState() => _GridFieldCellState(); +} + +class _GridFieldCellState extends State { + late PopoverController popoverController; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return FieldCellBloc(cellContext: widget.cellContext); + }, + child: BlocBuilder( + builder: (context, state) { + final button = AppFlowyPopover( + triggerActions: PopoverTriggerFlags.none, + constraints: const BoxConstraints(), + margin: EdgeInsets.zero, + direction: PopoverDirection.bottomWithLeftAligned, + controller: popoverController, + popupBuilder: (BuildContext context) { + return GridFieldCellActionSheet( + cellContext: widget.cellContext, + ); + }, + child: FieldCellButton( + field: widget.cellContext.field, + onTap: () => popoverController.show(), + ), + ); + + const line = Positioned( + top: 0, + bottom: 0, + right: 0, + child: _DragToExpandLine(), + ); + + return _GridHeaderCellContainer( + width: state.width, + child: Stack( + alignment: Alignment.centerRight, + fit: StackFit.expand, + children: [button, line], + ), + ); + }, + ), + ); + } +} + +class _GridHeaderCellContainer extends StatelessWidget { + final Widget child; + final double width; + const _GridHeaderCellContainer({ + required this.child, + required this.width, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final borderSide = BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0, + ); + final decoration = BoxDecoration( + border: Border( + top: borderSide, + right: borderSide, + bottom: borderSide, + ), + ); + + return Container( + width: width, + decoration: decoration, + child: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: child, + ), + ); + } +} + +class _DragToExpandLine extends StatelessWidget { + const _DragToExpandLine({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragUpdate: (value) { + context + .read() + .add(FieldCellEvent.startUpdateWidth(value.delta.dx)); + }, + onHorizontalDragEnd: (end) { + context + .read() + .add(const FieldCellEvent.endUpdateWidth()); + }, + child: FlowyHover( + cursor: SystemMouseCursors.resizeLeftRight, + style: HoverStyle( + hoverColor: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.zero, + contentMargin: const EdgeInsets.only(left: 6), + ), + child: const SizedBox(width: 4), + ), + ), + ); + } +} + +class FieldCellButton extends StatelessWidget { + final VoidCallback onTap; + final FieldPB field; + final int? maxLines; + final BorderRadius? radius; + const FieldCellButton({ + required this.field, + required this.onTap, + this.maxLines = 1, + this.radius = BorderRadius.zero, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + // Using this technique to have proper text ellipsis + // https://github.com/flutter/flutter/issues/18761#issuecomment-812390920 + final text = Characters(field.name) + .replaceAll(Characters(''), Characters('\u{200B}')) + .toString(); + return FlowyButton( + hoverColor: AFThemeExtension.of(context).greyHover, + onTap: onTap, + leftIcon: FlowySvg( + name: field.fieldType.iconName(), + ), + radius: radius, + text: FlowyText.medium( + text, + maxLines: maxLines, + overflow: TextOverflow.ellipsis, + ), + margin: GridSize.cellContentInsets, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart new file mode 100644 index 0000000000..19f2ecc24c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -0,0 +1,251 @@ +import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.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:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:styled_widget/styled_widget.dart'; + +import '../../layout/sizes.dart'; +import 'field_editor.dart'; + +class GridFieldCellActionSheet extends StatefulWidget { + final FieldContext cellContext; + const GridFieldCellActionSheet({required this.cellContext, Key? key}) + : super(key: key); + + @override + State createState() => _GridFieldCellActionSheetState(); +} + +class _GridFieldCellActionSheetState extends State { + bool _showFieldEditor = false; + + @override + Widget build(BuildContext context) { + if (_showFieldEditor) { + final field = widget.cellContext.field; + return SizedBox( + width: 400, + child: FieldEditor( + viewId: widget.cellContext.viewId, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.cellContext.viewId, + field: field, + ), + ), + ); + } + return BlocProvider( + create: (context) => + getIt(param1: widget.cellContext), + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _EditFieldButton( + cellContext: widget.cellContext, + onTap: () { + setState(() => _showFieldEditor = true); + }, + ), + VSpace(GridSize.typeOptionSeparatorHeight), + _FieldOperationList(widget.cellContext), + ], + ), + ), + ).padding(all: 6.0); + } +} + +class _EditFieldButton extends StatelessWidget { + final FieldContext cellContext; + final void Function()? onTap; + const _EditFieldButton({required this.cellContext, Key? key, this.onTap}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + LocaleKeys.grid_field_editProperty.tr(), + color: AFThemeExtension.of(context).textColor, + ), + onTap: onTap, + ), + ); + }, + ); + } +} + +class _FieldOperationList extends StatelessWidget { + final FieldContext fieldInfo; + const _FieldOperationList(this.fieldInfo, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Flex( + direction: Axis.horizontal, + children: [ + _actionCell(FieldAction.hide), + HSpace(GridSize.typeOptionSeparatorHeight), + _actionCell(FieldAction.duplicate), + ], + ), + VSpace(GridSize.typeOptionSeparatorHeight), + Flex( + direction: Axis.horizontal, + children: [ + _actionCell(FieldAction.delete), + HSpace(GridSize.typeOptionSeparatorHeight), + const Spacer(), + ], + ), + ], + ); + } + + Widget _actionCell(FieldAction action) { + bool enable = true; + + // If the field is primary, delete and duplicate are disabled. + if (fieldInfo.field.isPrimary) { + switch (action) { + case FieldAction.hide: + break; + case FieldAction.duplicate: + enable = false; + break; + case FieldAction.delete: + enable = false; + break; + } + } + + return Flexible( + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FieldActionCell( + fieldInfo: fieldInfo, + action: action, + enable: enable, + ), + ), + ); + } +} + +class FieldActionCell extends StatelessWidget { + final FieldContext fieldInfo; + final FieldAction action; + final bool enable; + + const FieldActionCell({ + required this.fieldInfo, + required this.action, + required this.enable, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + disable: !enable, + text: FlowyText.medium( + action.title(), + color: enable + ? AFThemeExtension.of(context).textColor + : Theme.of(context).disabledColor, + ), + onTap: () => action.run(context, fieldInfo), + leftIcon: svgWidget( + action.iconName(), + color: enable + ? AFThemeExtension.of(context).textColor + : Theme.of(context).disabledColor, + ), + ); + } +} + +enum FieldAction { + hide, + duplicate, + delete, +} + +extension _FieldActionExtension on FieldAction { + String iconName() { + switch (this) { + case FieldAction.hide: + return 'grid/hide'; + case FieldAction.duplicate: + return 'grid/duplicate'; + case FieldAction.delete: + return 'grid/delete'; + } + } + + String title() { + switch (this) { + case FieldAction.hide: + return LocaleKeys.grid_field_hide.tr(); + case FieldAction.duplicate: + return LocaleKeys.grid_field_duplicate.tr(); + case FieldAction.delete: + return LocaleKeys.grid_field_delete.tr(); + } + } + + void run(BuildContext context, FieldContext fieldInfo) { + switch (this) { + case FieldAction.hide: + context + .read() + .add(const FieldActionSheetEvent.hideField()); + break; + case FieldAction.duplicate: + PopoverContainer.of(context).close(); + + FieldBackendService( + viewId: fieldInfo.viewId, + fieldId: fieldInfo.field.id, + ).duplicateField(); + + break; + case FieldAction.delete: + PopoverContainer.of(context).close(); + + NavigatorAlertDialog( + title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + confirm: () { + FieldBackendService( + viewId: fieldInfo.viewId, + fieldId: fieldInfo.field.id, + ).deleteField(); + }, + ).show(context); + + break; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart new file mode 100644 index 0000000000..49d1f59014 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart @@ -0,0 +1,278 @@ +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:dartz/dartz.dart' show none; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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:appflowy_backend/log.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import '../../layout/sizes.dart'; +import 'field_type_option_editor.dart'; + +class FieldEditor extends StatefulWidget { + final String viewId; + final bool isGroupingField; + final Function(String)? onDeleted; + final Function(String)? onHidden; + final FieldTypeOptionLoader typeOptionLoader; + + const FieldEditor({ + required this.viewId, + required this.typeOptionLoader, + this.isGroupingField = false, + this.onDeleted, + this.onHidden, + Key? key, + }) : super(key: key); + + @override + State createState() => _FieldEditorState(); +} + +class _FieldEditorState extends State { + late PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + super.initState(); + } + + @override + void dispose() { + popoverMutex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final List children = [ + FieldNameTextField(popoverMutex: popoverMutex), + if (widget.onDeleted != null) _addDeleteFieldButton(), + if (widget.onHidden != null) _addHideFieldButton(), + if (!widget.typeOptionLoader.field.isPrimary) + FieldTypeOptionCell(popoverMutex: popoverMutex), + ]; + return BlocProvider( + create: (context) { + return FieldEditorBloc( + isGroupField: widget.isGroupingField, + loader: widget.typeOptionLoader, + field: widget.typeOptionLoader.field, + )..add(const FieldEditorEvent.initial()); + }, + child: ListView.separated( + shrinkWrap: true, + itemCount: children.length, + itemBuilder: (context, index) => children[index], + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + padding: const EdgeInsets.symmetric(vertical: 12.0), + ), + ); + } + + Widget _addDeleteFieldButton() { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: _DeleteFieldButton( + popoverMutex: popoverMutex, + onDeleted: () { + state.field.fold( + () => Log.error('Can not delete the field'), + (field) => widget.onDeleted?.call(field.id), + ); + }, + ), + ); + }, + ); + } + + Widget _addHideFieldButton() { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: _HideFieldButton( + popoverMutex: popoverMutex, + onHidden: () { + state.field.fold( + () => Log.error('Can not hidden the field'), + (field) => widget.onHidden?.call(field.id), + ); + }, + ), + ); + }, + ); + } +} + +class FieldTypeOptionCell extends StatelessWidget { + final PopoverMutex popoverMutex; + + const FieldTypeOptionCell({ + Key? key, + required this.popoverMutex, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (p, c) => p.field != c.field, + builder: (context, state) { + return state.field.fold( + () => const SizedBox.shrink(), + (fieldInfo) { + final dataController = + context.read().dataController; + return FieldTypeOptionEditor( + dataController: dataController, + popoverMutex: popoverMutex, + ); + }, + ); + }, + ); + } +} + +class FieldNameTextField extends StatefulWidget { + final PopoverMutex popoverMutex; + const FieldNameTextField({ + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + State createState() => _FieldNameTextFieldState(); +} + +class _FieldNameTextFieldState extends State { + final textController = TextEditingController(); + FocusNode focusNode = FocusNode(); + + @override + void initState() { + focusNode.addListener(() { + if (focusNode.hasFocus) { + widget.popoverMutex.close(); + } + }); + + widget.popoverMutex.listenOnPopoverChanged(() { + if (focusNode.hasFocus) { + focusNode.unfocus(); + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (p, c) => p.field == none(), + listener: (context, state) { + textController.text = state.name; + focusNode.requestFocus(); + }, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.errorText != current.errorText, + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: FlowyTextField( + focusNode: focusNode, + controller: textController, + onSubmitted: (String _) => PopoverContainer.of(context).close(), + text: state.name, + errorText: state.errorText.isEmpty ? null : state.errorText, + onChanged: (newName) { + context + .read() + .add(FieldEditorEvent.updateName(newName)); + }, + ), + ); + }, + ), + ); + } +} + +class _DeleteFieldButton extends StatelessWidget { + final PopoverMutex popoverMutex; + final VoidCallback? onDeleted; + + const _DeleteFieldButton({ + required this.popoverMutex, + required this.onDeleted, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous != current, + builder: (context, state) { + final enable = !state.canDelete && !state.isGroupField; + final Widget button = FlowyButton( + disable: !enable, + text: FlowyText.medium( + LocaleKeys.grid_field_delete.tr(), + color: enable ? null : Theme.of(context).disabledColor, + ), + leftIcon: svgWidget( + 'grid/delete', + color: enable ? null : Theme.of(context).disabledColor, + ), + onTap: () { + if (enable) onDeleted?.call(); + }, + onHover: (_) => popoverMutex.close(), + ); + return SizedBox(height: GridSize.popoverItemHeight, child: button); + }, + ); + } +} + +class _HideFieldButton extends StatelessWidget { + final PopoverMutex popoverMutex; + final VoidCallback? onHidden; + + const _HideFieldButton({ + required this.popoverMutex, + required this.onHidden, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous != current, + builder: (context, state) { + final Widget button = FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_field_hide.tr(), + ), + leftIcon: svgWidget('grid/hide'), + onTap: () => onHidden?.call(), + onHover: (_) => popoverMutex.close(), + ); + return SizedBox(height: GridSize.popoverItemHeight, child: button); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart new file mode 100644 index 0000000000..848d750270 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart @@ -0,0 +1,55 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; + +extension FieldTypeListExtension on FieldType { + String iconName() { + switch (this) { + case FieldType.Checkbox: + return "grid/field/checkbox"; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return "grid/field/date"; + case FieldType.MultiSelect: + return "grid/field/multi_select"; + case FieldType.Number: + return "grid/field/number"; + case FieldType.RichText: + return "grid/field/text"; + case FieldType.SingleSelect: + return "grid/field/single_select"; + case FieldType.URL: + return "grid/field/url"; + case FieldType.Checklist: + return "grid/field/checklist"; + } + throw UnimplementedError; + } + + String title() { + switch (this) { + case FieldType.Checkbox: + return LocaleKeys.grid_field_checkboxFieldName.tr(); + case FieldType.DateTime: + return LocaleKeys.grid_field_dateFieldName.tr(); + case FieldType.LastEditedTime: + return LocaleKeys.grid_field_updatedAtFieldName.tr(); + case FieldType.CreatedTime: + return LocaleKeys.grid_field_createdAtFieldName.tr(); + case FieldType.MultiSelect: + return LocaleKeys.grid_field_multiSelectFieldName.tr(); + case FieldType.Number: + return LocaleKeys.grid_field_numberFieldName.tr(); + case FieldType.RichText: + return LocaleKeys.grid_field_textFieldName.tr(); + case FieldType.SingleSelect: + return LocaleKeys.grid_field_singleSelectFieldName.tr(); + case FieldType.URL: + return LocaleKeys.grid_field_urlFieldName.tr(); + case FieldType.Checklist: + return LocaleKeys.grid_field_checklistFieldName.tr(); + } + throw UnimplementedError; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart new file mode 100644 index 0000000000..81bc715015 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart @@ -0,0 +1,70 @@ +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import '../../layout/sizes.dart'; +import 'field_type_extension.dart'; + +typedef SelectFieldCallback = void Function(FieldType); + +class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { + final SelectFieldCallback onSelectField; + const FieldTypeList({required this.onSelectField, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final cells = FieldType.values.map((fieldType) { + return FieldTypeCell( + fieldType: fieldType, + onSelectField: (fieldType) { + onSelectField(fieldType); + PopoverContainer.of(context).closeAll(); + }, + ); + }).toList(); + + return SizedBox( + width: 140, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ), + ); + } +} + +class FieldTypeCell extends StatelessWidget { + final FieldType fieldType; + final SelectFieldCallback onSelectField; + const FieldTypeCell({ + required this.fieldType, + required this.onSelectField, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium( + fieldType.title(), + ), + onTap: () => onSelectField(fieldType), + leftIcon: FlowySvg( + name: fieldType.iconName(), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart new file mode 100644 index 0000000000..9b9d792933 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart @@ -0,0 +1,126 @@ +import 'dart:typed_data'; +import 'package:appflowy/plugins/database_view/application/field/field_type_option_edit_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_controller.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:dartz/dartz.dart' show Either; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../layout/sizes.dart'; +import 'field_type_extension.dart'; +import 'field_type_list.dart'; +import 'type_option/builder.dart'; + +typedef UpdateFieldCallback = void Function(FieldPB, Uint8List); +typedef SwitchToFieldCallback = Future> + Function( + String fieldId, + FieldType fieldType, +); + +class FieldTypeOptionEditor extends StatelessWidget { + final TypeOptionController _dataController; + final PopoverMutex popoverMutex; + + const FieldTypeOptionEditor({ + required TypeOptionController dataController, + required this.popoverMutex, + Key? key, + }) : _dataController = dataController, + super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + final bloc = FieldTypeOptionEditBloc(_dataController); + bloc.add(const FieldTypeOptionEditEvent.initial()); + return bloc; + }, + child: BlocBuilder( + builder: (context, state) { + final typeOptionWidget = _typeOptionWidget( + context: context, + state: state, + ); + + final List children = [ + SwitchFieldButton(popoverMutex: popoverMutex), + if (typeOptionWidget != null) typeOptionWidget + ]; + + return ListView( + shrinkWrap: true, + children: children, + ); + }, + ), + ); + } + + Widget? _typeOptionWidget({ + required BuildContext context, + required FieldTypeOptionEditState state, + }) { + return makeTypeOptionWidget( + context: context, + dataController: _dataController, + popoverMutex: popoverMutex, + ); + } +} + +class SwitchFieldButton extends StatelessWidget { + final PopoverMutex popoverMutex; + const SwitchFieldButton({ + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final widget = AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(460, 540)), + asBarrier: true, + triggerActions: PopoverTriggerFlags.click, + mutex: popoverMutex, + offset: const Offset(8, 0), + popupBuilder: (popOverContext) { + return FieldTypeList( + onSelectField: (newFieldType) { + context + .read() + .add(FieldTypeOptionEditEvent.switchToField(newFieldType)); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: _buildMoreButton(context), + ), + ); + + return SizedBox( + height: GridSize.popoverItemHeight, + child: widget, + ); + } + + Widget _buildMoreButton(BuildContext context) { + final bloc = context.read(); + return FlowyButton( + text: FlowyText.medium( + bloc.state.field.fieldType.title(), + ), + leftIcon: FlowySvg(name: bloc.state.field.fieldType.iconName()), + rightIcon: const FlowySvg(name: 'grid/more'), + ); + } +} + +abstract class TypeOptionWidget extends StatelessWidget { + const TypeOptionWidget({Key? key}) : super(key: key); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart new file mode 100644 index 0000000000..b04440cede --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart @@ -0,0 +1,263 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:reorderables/reorderables.dart'; +import '../../../../application/field/type_option/type_option_service.dart'; +import '../../layout/sizes.dart'; +import 'field_editor.dart'; +import 'field_cell.dart'; + +class GridHeaderSliverAdaptor extends StatefulWidget { + final String viewId; + final FieldController fieldController; + final ScrollController anchorScrollController; + + const GridHeaderSliverAdaptor({ + required this.viewId, + required this.fieldController, + required this.anchorScrollController, + Key? key, + }) : super(key: key); + + @override + State createState() => + _GridHeaderSliverAdaptorState(); +} + +class _GridHeaderSliverAdaptorState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + final bloc = getIt( + param1: widget.viewId, + param2: widget.fieldController, + ); + bloc.add(const GridHeaderEvent.initial()); + return bloc; + }, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.fields.length != current.fields.length, + builder: (context, state) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: widget.anchorScrollController, + child: SizedBox( + height: GridSize.headerHeight, + child: _GridHeader(viewId: widget.viewId), + ), + ); + }, + ), + ); + } +} + +class _GridHeader extends StatefulWidget { + final String viewId; + const _GridHeader({Key? key, required this.viewId}) : super(key: key); + + @override + State<_GridHeader> createState() => _GridHeaderState(); +} + +class _GridHeaderState extends State<_GridHeader> { + final Map> _gridMap = {}; + + /// This is a workaround for [ReorderableRow]. + /// [ReorderableRow] warps the child's key with a [GlobalKey]. + /// It will trigger the child's widget's to recreate. + /// The state will lose. + _getKeyById(String id) { + if (_gridMap.containsKey(id)) { + return _gridMap[id]; + } + final newKey = ValueKey(id); + _gridMap[id] = newKey; + return newKey; + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.fields != current.fields, + builder: (context, state) { + final cells = state.fields + .where((field) => field.visibility) + .map( + (field) => FieldContext( + viewId: widget.viewId, + field: field.field, + ), + ) + .map( + (ctx) => GridFieldCell( + key: _getKeyById(ctx.field.id), + cellContext: ctx, + ), + ) + .toList(); + + return Container( + color: Theme.of(context).colorScheme.surface, + child: RepaintBoundary( + child: ReorderableRow( + crossAxisAlignment: CrossAxisAlignment.stretch, + scrollController: ScrollController(), + header: const _CellLeading(), + needsLongPressDraggable: false, + footer: _CellTrailing(viewId: widget.viewId), + onReorder: (int oldIndex, int newIndex) { + _onReorder(cells, oldIndex, context, newIndex); + }, + children: cells, + ), + ), + ); + }, + ); + } + + void _onReorder( + List cells, + int oldIndex, + BuildContext context, + int newIndex, + ) { + if (cells.length > oldIndex) { + final field = cells[oldIndex].cellContext.field; + context + .read() + .add(GridHeaderEvent.moveField(field, oldIndex, newIndex)); + } + } +} + +class _CellLeading extends StatelessWidget { + const _CellLeading({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: GridSize.leadingHeaderPadding, + ); + } +} + +class _CellTrailing extends StatelessWidget { + final String viewId; + const _CellTrailing({required this.viewId, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final borderSide = + BorderSide(color: Theme.of(context).dividerColor, width: 1.0); + return Container( + width: GridSize.trailHeaderPadding, + decoration: BoxDecoration( + border: Border(top: borderSide, bottom: borderSide), + ), + padding: GridSize.headerContentInsets, + child: CreateFieldButton(viewId: viewId), + ); + } +} + +class CreateFieldButton extends StatefulWidget { + final String viewId; + const CreateFieldButton({required this.viewId, Key? key}) : super(key: key); + + @override + State createState() => _CreateFieldButtonState(); +} + +class _CreateFieldButtonState extends State { + final popoverController = PopoverController(); + late TypeOptionPB typeOption; + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithRightAligned, + asBarrier: true, + margin: EdgeInsets.zero, + constraints: BoxConstraints.loose(const Size(240, 600)), + triggerActions: PopoverTriggerFlags.none, + child: FlowyButton( + radius: BorderRadius.zero, + text: FlowyText.medium(LocaleKeys.grid_field_newProperty.tr()), + hoverColor: AFThemeExtension.of(context).greyHover, + onTap: () async { + final result = await TypeOptionBackendService.createFieldTypeOption( + viewId: widget.viewId, + ); + result.fold( + (l) { + typeOption = l; + popoverController.show(); + }, + (r) => Log.error("Failed to create field type option: $r"), + ); + }, + leftIcon: const FlowySvg(name: 'home/add'), + ), + popupBuilder: (BuildContext popover) { + return FieldEditor( + viewId: widget.viewId, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.viewId, + field: typeOption.field_2, + ), + ); + }, + ); + } +} + +class SliverHeaderDelegateImplementation + extends SliverPersistentHeaderDelegate { + final String gridId; + final List fields; + + SliverHeaderDelegateImplementation({ + required this.gridId, + required this.fields, + }); + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return _GridHeader(viewId: gridId); + } + + @override + double get maxExtent => GridSize.headerHeight; + + @override + double get minExtent => GridSize.headerHeight; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + if (oldDelegate is SliverHeaderDelegateImplementation) { + return fields.length != oldDelegate.fields.length; + } + return true; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart new file mode 100644 index 0000000000..1047031a4f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart @@ -0,0 +1,246 @@ +import 'dart:typed_data'; + +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_controller.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:protobuf/protobuf.dart' hide FieldInfo; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import 'checkbox.dart'; +import 'checklist.dart'; +import 'date.dart'; +import 'multi_select.dart'; +import 'number.dart'; +import 'rich_text.dart'; +import 'single_select.dart'; +import 'url.dart'; + +typedef TypeOptionData = Uint8List; +typedef TypeOptionDataCallback = void Function(TypeOptionData typeOptionData); +typedef ShowOverlayCallback = void Function( + BuildContext anchorContext, + Widget child, { + VoidCallback? onRemoved, +}); +typedef HideOverlayCallback = void Function(BuildContext anchorContext); + +class TypeOptionOverlayDelegate { + ShowOverlayCallback showOverlay; + HideOverlayCallback hideOverlay; + TypeOptionOverlayDelegate({ + required this.showOverlay, + required this.hideOverlay, + }); +} + +abstract class TypeOptionWidgetBuilder { + Widget? build(BuildContext context); +} + +Widget? makeTypeOptionWidget({ + required BuildContext context, + required TypeOptionController dataController, + required PopoverMutex popoverMutex, +}) { + final builder = makeTypeOptionWidgetBuilder( + dataController: dataController, + popoverMutex: popoverMutex, + ); + return builder.build(context); +} + +TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ + required TypeOptionController dataController, + required PopoverMutex popoverMutex, +}) { + final viewId = dataController.loader.viewId; + final fieldType = dataController.field.fieldType; + + switch (dataController.field.fieldType) { + case FieldType.Checkbox: + return CheckboxTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + popoverMutex, + ); + case FieldType.SingleSelect: + return SingleSelectTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + popoverMutex, + ); + case FieldType.MultiSelect: + return MultiSelectTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + popoverMutex, + ); + case FieldType.Number: + return NumberTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + popoverMutex, + ); + case FieldType.RichText: + return RichTextTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + + case FieldType.URL: + return URLTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + + case FieldType.Checklist: + return ChecklistTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + } + throw UnimplementedError; +} + +TypeOptionContext makeTypeOptionContext({ + required String viewId, + required FieldInfo fieldInfo, +}) { + final loader = FieldTypeOptionLoader(viewId: viewId, field: fieldInfo.field); + final dataController = TypeOptionController( + loader: loader, + field: fieldInfo.field, + ); + return makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldInfo.fieldType, + dataController: dataController, + ); +} + +TypeOptionContext makeSingleSelectTypeOptionContext({ + required String viewId, + required FieldPB fieldPB, +}) { + return makeSelectTypeOptionContext(viewId: viewId, fieldPB: fieldPB); +} + +TypeOptionContext makeMultiSelectTypeOptionContext({ + required String viewId, + required FieldPB fieldPB, +}) { + return makeSelectTypeOptionContext(viewId: viewId, fieldPB: fieldPB); +} + +TypeOptionContext makeSelectTypeOptionContext({ + required String viewId, + required FieldPB fieldPB, +}) { + final loader = FieldTypeOptionLoader( + viewId: viewId, + field: fieldPB, + ); + final dataController = TypeOptionController( + loader: loader, + field: fieldPB, + ); + final typeOptionContext = makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldPB.fieldType, + dataController: dataController, + ); + return typeOptionContext; +} + +TypeOptionContext + makeTypeOptionContextWithDataController({ + required String viewId, + required FieldType fieldType, + required TypeOptionController dataController, +}) { + switch (fieldType) { + case FieldType.Checkbox: + return CheckboxTypeOptionContext( + dataController: dataController, + dataParser: CheckboxTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateTypeOptionContext( + dataController: dataController, + dataParser: DateTypeOptionDataParser(), + ) as TypeOptionContext; + case FieldType.SingleSelect: + return SingleSelectTypeOptionContext( + dataController: dataController, + dataParser: SingleSelectTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.MultiSelect: + return MultiSelectTypeOptionContext( + dataController: dataController, + dataParser: MultiSelectTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.Checklist: + return ChecklistTypeOptionContext( + dataController: dataController, + dataParser: ChecklistTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.Number: + return NumberTypeOptionContext( + dataController: dataController, + dataParser: NumberTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.RichText: + return RichTextTypeOptionContext( + dataController: dataController, + dataParser: RichTextTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + + case FieldType.URL: + return URLTypeOptionContext( + dataController: dataController, + dataParser: URLTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + } + + throw UnimplementedError; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/checkbox.dart new file mode 100644 index 0000000000..13c20d5c4d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/checkbox.dart @@ -0,0 +1,10 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:flutter/material.dart'; +import 'builder.dart'; + +class CheckboxTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + CheckboxTypeOptionWidgetBuilder(CheckboxTypeOptionContext typeOptionContext); + + @override + Widget? build(BuildContext context) => null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/checklist.dart new file mode 100644 index 0000000000..c010df7b96 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/checklist.dart @@ -0,0 +1,12 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:flutter/material.dart'; +import 'builder.dart'; + +class ChecklistTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + ChecklistTypeOptionWidgetBuilder( + ChecklistTypeOptionContext typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart new file mode 100644 index 0000000000..da81368c50 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart @@ -0,0 +1,340 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart' hide DateFormat; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flowy_infra/image.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_popover/appflowy_popover.dart'; +import '../../../layout/sizes.dart'; +import '../../common/type_option_separator.dart'; +import '../field_type_option_editor.dart'; +import 'builder.dart'; + +class DateTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + final DateTypeOptionWidget _widget; + + DateTypeOptionWidgetBuilder( + DateTypeOptionContext typeOptionContext, + PopoverMutex popoverMutex, + ) : _widget = DateTypeOptionWidget( + typeOptionContext: typeOptionContext, + popoverMutex: popoverMutex, + ); + + @override + Widget? build(BuildContext context) { + return _widget; + } +} + +class DateTypeOptionWidget extends TypeOptionWidget { + final DateTypeOptionContext typeOptionContext; + final PopoverMutex popoverMutex; + const DateTypeOptionWidget({ + required this.typeOptionContext, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + DateTypeOptionBloc(typeOptionContext: typeOptionContext), + child: BlocConsumer( + listener: (context, state) => + typeOptionContext.typeOption = state.typeOption, + builder: (context, state) { + final List children = [ + const TypeOptionSeparator(), + _renderDateFormatButton(context, state.typeOption.dateFormat), + _renderTimeFormatButton(context, state.typeOption.timeFormat), + ]; + + return ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) { + if (index == 0) { + return const SizedBox(); + } else { + return VSpace(GridSize.typeOptionSeparatorHeight); + } + }, + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ); + }, + ), + ); + } + + Widget _renderDateFormatButton( + BuildContext context, + DateFormatPB dataFormat, + ) { + return AppFlowyPopover( + mutex: popoverMutex, + asBarrier: true, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + constraints: BoxConstraints.loose(const Size(460, 440)), + popupBuilder: (popoverContext) { + return DateFormatList( + selectedFormat: dataFormat, + onSelected: (format) { + context + .read() + .add(DateTypeOptionEvent.didSelectDateFormat(format)); + PopoverContainer.of(popoverContext).close(); + }, + ); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: DateFormatButton(), + ), + ); + } + + Widget _renderTimeFormatButton( + BuildContext context, + TimeFormatPB timeFormat, + ) { + return AppFlowyPopover( + mutex: popoverMutex, + asBarrier: true, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + constraints: BoxConstraints.loose(const Size(460, 440)), + popupBuilder: (BuildContext popoverContext) { + return TimeFormatList( + selectedFormat: timeFormat, + onSelected: (format) { + context + .read() + .add(DateTypeOptionEvent.didSelectTimeFormat(format)); + PopoverContainer.of(popoverContext).close(); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: TimeFormatButton(timeFormat: timeFormat), + ), + ); + } +} + +class DateFormatButton extends StatelessWidget { + final VoidCallback? onTap; + final void Function(bool)? onHover; + const DateFormatButton({ + this.onTap, + this.onHover, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()), + onTap: onTap, + onHover: onHover, + rightIcon: const FlowySvg(name: 'grid/more'), + ), + ); + } +} + +class TimeFormatButton extends StatelessWidget { + final TimeFormatPB timeFormat; + final VoidCallback? onTap; + final void Function(bool)? onHover; + const TimeFormatButton({ + required this.timeFormat, + this.onTap, + this.onHover, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()), + onTap: onTap, + onHover: onHover, + rightIcon: const FlowySvg(name: 'grid/more'), + ), + ); + } +} + +class DateFormatList extends StatelessWidget { + final DateFormatPB selectedFormat; + final Function(DateFormatPB format) onSelected; + const DateFormatList({ + required this.selectedFormat, + required this.onSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final cells = DateFormatPB.values.map((format) { + return DateFormatCell( + dateFormat: format, + onSelected: onSelected, + isSelected: selectedFormat == format, + ); + }).toList(); + + return SizedBox( + width: 180, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ), + ); + } +} + +class DateFormatCell extends StatelessWidget { + final bool isSelected; + final DateFormatPB dateFormat; + final Function(DateFormatPB format) onSelected; + const DateFormatCell({ + required this.dateFormat, + required this.onSelected, + required this.isSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget? checkmark; + if (isSelected) { + checkmark = const FlowySvg(name: 'grid/checkmark'); + } + + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(dateFormat.title()), + rightIcon: checkmark, + onTap: () => onSelected(dateFormat), + ), + ); + } +} + +extension DateFormatExtension on DateFormatPB { + String title() { + switch (this) { + case DateFormatPB.Friendly: + return LocaleKeys.grid_field_dateFormatFriendly.tr(); + case DateFormatPB.ISO: + return LocaleKeys.grid_field_dateFormatISO.tr(); + case DateFormatPB.Local: + return LocaleKeys.grid_field_dateFormatLocal.tr(); + case DateFormatPB.US: + return LocaleKeys.grid_field_dateFormatUS.tr(); + case DateFormatPB.DayMonthYear: + return LocaleKeys.grid_field_dateFormatDayMonthYear.tr(); + default: + throw UnimplementedError; + } + } +} + +class TimeFormatList extends StatelessWidget { + final TimeFormatPB selectedFormat; + final Function(TimeFormatPB format) onSelected; + const TimeFormatList({ + required this.selectedFormat, + required this.onSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final cells = TimeFormatPB.values.map((format) { + return TimeFormatCell( + isSelected: format == selectedFormat, + timeFormat: format, + onSelected: onSelected, + ); + }).toList(); + + return SizedBox( + width: 120, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ), + ); + } +} + +class TimeFormatCell extends StatelessWidget { + final TimeFormatPB timeFormat; + final bool isSelected; + final Function(TimeFormatPB format) onSelected; + const TimeFormatCell({ + required this.timeFormat, + required this.onSelected, + required this.isSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget? checkmark; + if (isSelected) { + checkmark = const FlowySvg(name: 'grid/checkmark'); + } + + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(timeFormat.title()), + rightIcon: checkmark, + onTap: () => onSelected(timeFormat), + ), + ); + } +} + +extension TimeFormatExtension on TimeFormatPB { + String title() { + switch (this) { + case TimeFormatPB.TwelveHour: + return LocaleKeys.grid_field_timeFormatTwelveHour.tr(); + case TimeFormatPB.TwentyFourHour: + return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr(); + default: + throw UnimplementedError; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/multi_select.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/multi_select.dart new file mode 100644 index 0000000000..bc71a313c5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/multi_select.dart @@ -0,0 +1,51 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/multi_select_type_option.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; + +import '../field_type_option_editor.dart'; +import 'builder.dart'; +import 'select_option.dart'; + +class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + final MultiSelectTypeOptionWidget _widget; + + MultiSelectTypeOptionWidgetBuilder( + MultiSelectTypeOptionContext typeOptionContext, + PopoverMutex popoverMutex, + ) : _widget = MultiSelectTypeOptionWidget( + selectOptionAction: MultiSelectAction( + fieldId: typeOptionContext.fieldId, + viewId: typeOptionContext.viewId, + typeOptionContext: typeOptionContext, + ), + popoverMutex: popoverMutex, + ); + + @override + Widget? build(BuildContext context) => _widget; +} + +class MultiSelectTypeOptionWidget extends TypeOptionWidget { + final MultiSelectAction selectOptionAction; + final PopoverMutex? popoverMutex; + + const MultiSelectTypeOptionWidget({ + Key? key, + required this.selectOptionAction, + this.popoverMutex, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SelectOptionTypeOptionWidget( + options: selectOptionAction.typeOption.options, + beginEdit: () { + PopoverContainer.of(context).closeAll(); + }, + popoverMutex: popoverMutex, + typeOptionAction: selectOptionAction, + // key: ValueKey(state.typeOption.hashCode), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart new file mode 100644 index 0000000000..8bbfeaacbf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart @@ -0,0 +1,213 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/number_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +import '../../../layout/sizes.dart'; +import '../../common/type_option_separator.dart'; +import '../field_type_option_editor.dart'; +import 'builder.dart'; + +class NumberTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + final NumberTypeOptionWidget _widget; + + NumberTypeOptionWidgetBuilder( + NumberTypeOptionContext typeOptionContext, + PopoverMutex popoverMutex, + ) : _widget = NumberTypeOptionWidget( + typeOptionContext: typeOptionContext, + popoverMutex: popoverMutex, + ); + + @override + Widget? build(BuildContext context) { + return Column( + children: [ + VSpace(GridSize.typeOptionSeparatorHeight), + const TypeOptionSeparator(), + _widget, + ], + ); + } +} + +class NumberTypeOptionWidget extends TypeOptionWidget { + final NumberTypeOptionContext typeOptionContext; + final PopoverMutex popoverMutex; + const NumberTypeOptionWidget({ + required this.typeOptionContext, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + NumberTypeOptionBloc(typeOptionContext: typeOptionContext), + child: BlocConsumer( + listener: (context, state) => + typeOptionContext.typeOption = state.typeOption, + builder: (context, state) { + final selectNumUnitButton = SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + margin: GridSize.typeOptionContentInsets, + rightIcon: const FlowySvg(name: 'grid/more'), + text: FlowyText.regular( + state.typeOption.format.title(), + ), + ), + ); + + final numFormatTitle = Container( + padding: const EdgeInsets.only(left: 6), + height: GridSize.popoverItemHeight, + alignment: Alignment.centerLeft, + child: FlowyText.medium( + LocaleKeys.grid_field_numberFormat.tr(), + ), + ); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + numFormatTitle, + AppFlowyPopover( + mutex: popoverMutex, + triggerActions: + PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + constraints: BoxConstraints.loose(const Size(460, 440)), + margin: EdgeInsets.zero, + child: selectNumUnitButton, + popupBuilder: (BuildContext popoverContext) { + return NumberFormatList( + onSelected: (format) { + context + .read() + .add(NumberTypeOptionEvent.didSelectFormat(format)); + PopoverContainer.of(popoverContext).close(); + }, + selectedFormat: state.typeOption.format, + ); + }, + ), + ], + ), + ); + }, + ), + ); + } +} + +typedef SelectNumberFormatCallback = Function(NumberFormatPB format); + +class NumberFormatList extends StatelessWidget { + final SelectNumberFormatCallback onSelected; + final NumberFormatPB selectedFormat; + const NumberFormatList({ + required this.selectedFormat, + required this.onSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => NumberFormatBloc(), + child: SizedBox( + width: 180, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const _FilterTextField(), + const TypeOptionSeparator(spacing: 0.0), + BlocBuilder( + builder: (context, state) { + final cells = state.formats.map((format) { + return NumberFormatCell( + isSelected: format == selectedFormat, + format: format, + onSelected: (format) { + onSelected(format); + }, + ); + }).toList(); + + final list = ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + padding: const EdgeInsets.all(6.0), + ); + return Flexible(child: list); + }, + ), + ], + ), + ), + ); + } +} + +class NumberFormatCell extends StatelessWidget { + final NumberFormatPB format; + final bool isSelected; + final Function(NumberFormatPB format) onSelected; + const NumberFormatCell({ + required this.isSelected, + required this.format, + required this.onSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget? checkmark; + if (isSelected) { + checkmark = const FlowySvg( + name: 'grid/checkmark', + ); + } + + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(format.title()), + onTap: () => onSelected(format), + rightIcon: checkmark, + ), + ); + } +} + +class _FilterTextField extends StatelessWidget { + const _FilterTextField({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(6.0), + child: FlowyTextField( + onChanged: (text) => context + .read() + .add(NumberFormatEvent.setFilter(text)), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/rich_text.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/rich_text.dart new file mode 100644 index 0000000000..4ce086fc43 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/rich_text.dart @@ -0,0 +1,10 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:flutter/material.dart'; +import 'builder.dart'; + +class RichTextTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + RichTextTypeOptionWidgetBuilder(RichTextTypeOptionContext typeOptionContext); + + @override + Widget? build(BuildContext context) => null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart new file mode 100644 index 0000000000..c7fb5bab13 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart @@ -0,0 +1,327 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.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:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +import '../../../layout/sizes.dart'; +import '../../../../../widgets/row/cells/select_option_cell/extension.dart'; +import '../../common/type_option_separator.dart'; +import 'select_option_editor.dart'; + +class SelectOptionTypeOptionWidget extends StatelessWidget { + final List options; + final VoidCallback beginEdit; + final ISelectOptionAction typeOptionAction; + final PopoverMutex? popoverMutex; + + const SelectOptionTypeOptionWidget({ + required this.options, + required this.beginEdit, + required this.typeOptionAction, + this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SelectOptionTypeOptionBloc( + options: options, + typeOptionAction: typeOptionAction, + ), + child: + BlocBuilder( + builder: (context, state) { + final List children = [ + const TypeOptionSeparator(), + const OptionTitle(), + if (state.isEditingOption) + _CreateOptionTextField(popoverMutex: popoverMutex), + if (state.options.isNotEmpty && state.isEditingOption) + const VSpace(10), + if (state.options.isEmpty && !state.isEditingOption) + const _AddOptionButton(), + _OptionList(popoverMutex: popoverMutex) + ]; + + return ListView.builder( + shrinkWrap: true, + itemCount: children.length, + itemBuilder: (context, index) { + return children[index]; + }, + ); + }, + ), + ); + } +} + +class OptionTitle extends StatelessWidget { + const OptionTitle({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final List children = [ + Padding( + padding: const EdgeInsets.only(left: 9), + child: FlowyText.medium( + LocaleKeys.grid_field_optionTitle.tr(), + ), + ) + ]; + if (state.options.isNotEmpty && !state.isEditingOption) { + children.add(const Spacer()); + children.add(const _OptionTitleButton()); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: Row(children: children), + ), + ); + }, + ); + } +} + +class _OptionTitleButton extends StatelessWidget { + const _OptionTitleButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 100, + height: 26, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_field_addOption.tr(), + textAlign: TextAlign.center, + ), + onTap: () { + context + .read() + .add(const SelectOptionTypeOptionEvent.addingOption()); + }, + ), + ); + } +} + +class _OptionList extends StatelessWidget { + final PopoverMutex? popoverMutex; + const _OptionList({Key? key, this.popoverMutex}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) { + return previous.options != current.options; + }, + builder: (context, state) { + final cells = state.options.map((option) { + return _makeOptionCell( + context: context, + option: option, + popoverMutex: popoverMutex, + ); + }).toList(); + + return ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ); + }, + ); + } + + _OptionCell _makeOptionCell({ + required BuildContext context, + required SelectOptionPB option, + PopoverMutex? popoverMutex, + }) { + return _OptionCell( + option: option, + popoverMutex: popoverMutex, + ); + } +} + +class _OptionCell extends StatefulWidget { + final SelectOptionPB option; + final PopoverMutex? popoverMutex; + const _OptionCell({required this.option, Key? key, this.popoverMutex}) + : super(key: key); + + @override + State<_OptionCell> createState() => _OptionCellState(); +} + +class _OptionCellState extends State<_OptionCell> { + late PopoverController _popoverController; + + @override + void initState() { + _popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final child = SizedBox( + height: GridSize.popoverItemHeight, + child: SelectOptionTagCell( + option: widget.option, + onSelected: (SelectOptionPB pb) { + _popoverController.show(); + }, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: svgWidget( + "grid/details", + color: Theme.of(context).iconTheme.color, + ), + ), + ], + ), + ); + return AppFlowyPopover( + controller: _popoverController, + mutex: widget.popoverMutex, + offset: const Offset(8, 0), + margin: EdgeInsets.zero, + asBarrier: true, + constraints: BoxConstraints.loose(const Size(460, 470)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: child, + ), + popupBuilder: (BuildContext popoverContext) { + return SelectOptionTypeOptionEditor( + option: widget.option, + onDeleted: () { + context + .read() + .add(SelectOptionTypeOptionEvent.deleteOption(widget.option)); + PopoverContainer.of(popoverContext).close(); + }, + onUpdated: (updatedOption) { + context + .read() + .add(SelectOptionTypeOptionEvent.updateOption(updatedOption)); + PopoverContainer.of(popoverContext).close(); + }, + key: ValueKey(widget.option.id), + ); + }, + ); + } +} + +class _AddOptionButton extends StatelessWidget { + const _AddOptionButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + LocaleKeys.grid_field_addSelectOption.tr(), + color: AFThemeExtension.of(context).textColor, + ), + onTap: () { + context + .read() + .add(const SelectOptionTypeOptionEvent.addingOption()); + }, + leftIcon: svgWidget( + "home/add", + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ); + } +} + +class _CreateOptionTextField extends StatefulWidget { + final PopoverMutex? popoverMutex; + const _CreateOptionTextField({ + Key? key, + this.popoverMutex, + }) : super(key: key); + + @override + State<_CreateOptionTextField> createState() => _CreateOptionTextFieldState(); +} + +class _CreateOptionTextFieldState extends State<_CreateOptionTextField> { + late final FocusNode _focusNode; + + @override + void initState() { + _focusNode = FocusNode(); + _focusNode.addListener(() { + if (_focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + }); + widget.popoverMutex?.listenOnPopoverChanged(() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final text = state.newOptionName.foldRight("", (a, previous) => a); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: FlowyTextField( + autoClearWhenDone: true, + maxLength: 30, + text: text, + focusNode: _focusNode, + onCanceled: () { + context + .read() + .add(const SelectOptionTypeOptionEvent.endAddingOption()); + }, + onEditingComplete: () {}, + onSubmitted: (optionName) { + context + .read() + .add(SelectOptionTypeOptionEvent.createOption(optionName)); + }, + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart new file mode 100644 index 0000000000..7225dc603b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart @@ -0,0 +1,249 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +import '../../../layout/sizes.dart'; +import '../../common/type_option_separator.dart'; + +class SelectOptionTypeOptionEditor extends StatelessWidget { + final SelectOptionPB option; + final VoidCallback onDeleted; + final Function(SelectOptionPB) onUpdated; + final bool showOptions; + final bool autoFocus; + const SelectOptionTypeOptionEditor({ + required this.option, + required this.onDeleted, + required this.onUpdated, + this.showOptions = true, + this.autoFocus = true, + Key? key, + }) : super(key: key); + + static String get identifier => (SelectOptionTypeOptionEditor).toString(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditSelectOptionBloc(option: option), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.deleted != c.deleted, + listener: (context, state) { + state.deleted.fold(() => null, (_) => onDeleted()); + }, + ), + BlocListener( + listenWhen: (p, c) => p.option != c.option, + listener: (context, state) { + onUpdated(state.option); + }, + ), + ], + child: BlocBuilder( + builder: (context, state) { + final List cells = [ + _OptionNameTextField( + name: state.option.name, + autoFocus: autoFocus, + ), + const VSpace(10), + const _DeleteTag(), + ]; + + if (showOptions) { + cells.add(const TypeOptionSeparator()); + cells.add( + SelectOptionColorList( + selectedColor: state.option.color, + onSelectedColor: (color) => context + .read() + .add(EditSelectOptionEvent.updateColor(color)), + ), + ); + } + + return SizedBox( + width: 180, + child: ListView.builder( + shrinkWrap: true, + controller: ScrollController(), + physics: StyledScrollPhysics(), + itemCount: cells.length, + itemBuilder: (context, index) { + if (cells[index] is TypeOptionSeparator) { + return cells[index]; + } else { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: cells[index], + ); + } + }, + padding: const EdgeInsets.symmetric(vertical: 6.0), + ), + ); + }, + ), + ), + ); + } +} + +class _DeleteTag extends StatelessWidget { + const _DeleteTag({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_selectOption_deleteTag.tr(), + ), + leftIcon: const FlowySvg(name: 'grid/delete'), + onTap: () { + context + .read() + .add(const EditSelectOptionEvent.delete()); + }, + ), + ); + } +} + +class _OptionNameTextField extends StatelessWidget { + final String name; + final bool autoFocus; + const _OptionNameTextField({ + required this.name, + required this.autoFocus, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyTextField( + autoFocus: autoFocus, + text: name, + maxLength: 30, + submitOnLeave: true, + onSubmitted: (newName) { + if (name != newName) { + context + .read() + .add(EditSelectOptionEvent.updateName(newName)); + } + }, + ); + } +} + +class SelectOptionColorList extends StatelessWidget { + const SelectOptionColorList({ + super.key, + this.selectedColor, + required this.onSelectedColor, + }); + + final SelectOptionColorPB? selectedColor; + final void Function(SelectOptionColorPB color) onSelectedColor; + + @override + Widget build(BuildContext context) { + final cells = SelectOptionColorPB.values.map((color) { + return _SelectOptionColorCell( + color: color, + isSelected: selectedColor != null ? selectedColor == color : false, + onSelectedColor: onSelectedColor, + ); + }).toList(); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: GridSize.typeOptionContentInsets, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyText.medium( + LocaleKeys.grid_selectOption_colorPanelTitle.tr(), + textAlign: TextAlign.left, + color: Theme.of(context).hintColor, + ), + ), + ), + ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemCount: cells.length, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ), + ], + ); + } +} + +class _SelectOptionColorCell extends StatelessWidget { + const _SelectOptionColorCell({ + required this.color, + required this.isSelected, + required this.onSelectedColor, + Key? key, + }) : super(key: key); + + final SelectOptionColorPB color; + final bool isSelected; + final void Function(SelectOptionColorPB color) onSelectedColor; + + @override + Widget build(BuildContext context) { + Widget? checkmark; + if (isSelected) { + checkmark = svgWidget("grid/checkmark"); + } + + final colorIcon = SizedBox.square( + dimension: 16, + child: Container( + decoration: BoxDecoration( + color: color.toColor(context), + shape: BoxShape.circle, + ), + ), + ); + + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + color.optionName(), + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: colorIcon, + rightIcon: checkmark, + onTap: () => onSelectedColor(color), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/single_select.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/single_select.dart new file mode 100644 index 0000000000..4b8db69941 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/single_select.dart @@ -0,0 +1,50 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/single_select_type_option.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:flutter/material.dart'; +import '../field_type_option_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'builder.dart'; +import 'select_option.dart'; + +class SingleSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + final SingleSelectTypeOptionWidget _widget; + + SingleSelectTypeOptionWidgetBuilder( + SingleSelectTypeOptionContext singleSelectTypeOption, + PopoverMutex popoverMutex, + ) : _widget = SingleSelectTypeOptionWidget( + selectOptionAction: SingleSelectAction( + fieldId: singleSelectTypeOption.fieldId, + viewId: singleSelectTypeOption.viewId, + typeOptionContext: singleSelectTypeOption, + ), + popoverMutex: popoverMutex, + ); + + @override + Widget? build(BuildContext context) => _widget; +} + +class SingleSelectTypeOptionWidget extends TypeOptionWidget { + final SingleSelectAction selectOptionAction; + final PopoverMutex? popoverMutex; + + const SingleSelectTypeOptionWidget({ + Key? key, + required this.selectOptionAction, + this.popoverMutex, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SelectOptionTypeOptionWidget( + options: selectOptionAction.typeOption.options, + beginEdit: () { + PopoverContainer.of(context).closeAll(); + }, + popoverMutex: popoverMutex, + typeOptionAction: selectOptionAction, + // key: ValueKey(state.typeOption.hashCode), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/url.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/url.dart new file mode 100644 index 0000000000..38680299b2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/url.dart @@ -0,0 +1,10 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:flutter/material.dart'; +import 'builder.dart'; + +class URLTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + URLTypeOptionWidgetBuilder(URLTypeOptionContext typeOptionContext); + + @override + Widget? build(BuildContext context) => null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart new file mode 100644 index 0000000000..6c1f514371 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart @@ -0,0 +1,134 @@ +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../layout/sizes.dart'; + +class RowActions extends StatelessWidget { + final String viewId; + final RowId rowId; + const RowActions({ + required this.viewId, + required this.rowId, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RowActionSheetBloc(viewId: viewId, rowId: rowId), + child: BlocBuilder( + builder: (context, state) { + final cells = _RowAction.values + .where((value) => value.enable()) + .map((action) => _ActionCell(action: action)) + .toList(); + + // + final list = ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ); + return list; + }, + ), + ); + } +} + +class _ActionCell extends StatelessWidget { + final _RowAction action; + const _ActionCell({required this.action, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + action.title(), + color: action.enable() + ? AFThemeExtension.of(context).textColor + : Theme.of(context).disabledColor, + ), + onTap: () { + if (action.enable()) { + action.performAction(context); + } + }, + leftIcon: svgWidget( + action.iconName(), + color: Theme.of(context).iconTheme.color, + ), + ), + ); + } +} + +enum _RowAction { + delete, + duplicate, +} + +extension _RowActionExtension on _RowAction { + String iconName() { + switch (this) { + case _RowAction.duplicate: + return 'grid/duplicate'; + case _RowAction.delete: + return 'grid/delete'; + } + } + + String title() { + switch (this) { + case _RowAction.duplicate: + return LocaleKeys.grid_row_duplicate.tr(); + case _RowAction.delete: + return LocaleKeys.grid_row_delete.tr(); + } + } + + bool enable() { + switch (this) { + case _RowAction.duplicate: + return false; + case _RowAction.delete: + return true; + } + } + + void performAction(BuildContext context) { + switch (this) { + case _RowAction.duplicate: + context + .read() + .add(const RowActionSheetEvent.duplicateRow()); + break; + case _RowAction.delete: + context + .read() + .add(const RowActionSheetEvent.deleteRow()); + + break; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart new file mode 100755 index 0000000000..5c54665e46 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart @@ -0,0 +1,355 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.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'; +import 'package:provider/provider.dart'; + +import '../../../../widgets/row/accessory/cell_accessory.dart'; +import '../../layout/sizes.dart'; +import '../../../../widgets/row/cells/cell_container.dart'; +import 'action.dart'; +import "package:appflowy/generated/locale_keys.g.dart"; +import 'package:easy_localization/easy_localization.dart'; + +class GridRow extends StatefulWidget { + final RowId viewId; + final RowId rowId; + final RowController dataController; + final GridCellBuilder cellBuilder; + final void Function(BuildContext, GridCellBuilder) openDetailPage; + + final int? index; + final bool isDraggable; + + const GridRow({ + super.key, + required this.viewId, + required this.rowId, + required this.dataController, + required this.cellBuilder, + required this.openDetailPage, + this.index, + this.isDraggable = false, + }); + + @override + State createState() => _GridRowState(); +} + +class _GridRowState extends State { + late final RowBloc _rowBloc; + + @override + void initState() { + super.initState(); + _rowBloc = RowBloc( + rowId: widget.rowId, + dataController: widget.dataController, + viewId: widget.viewId, + ); + _rowBloc.add(const RowEvent.initial()); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _rowBloc, + child: _RowEnterRegion( + child: BlocBuilder( + // The row need to rebuild when the cell count changes. + buildWhen: (p, c) => p.cellByFieldId.length != c.cellByFieldId.length, + builder: (context, state) { + final content = Expanded( + child: RowContent( + builder: widget.cellBuilder, + onExpand: () => widget.openDetailPage( + context, + widget.cellBuilder, + ), + ), + ); + + return Row( + children: [ + _RowLeading( + index: widget.index, + isDraggable: widget.isDraggable, + ), + content, + ], + ); + }, + ), + ), + ); + } + + @override + Future dispose() async { + _rowBloc.close(); + super.dispose(); + } +} + +class _RowLeading extends StatefulWidget { + final int? index; + final bool isDraggable; + + const _RowLeading({ + this.index, + this.isDraggable = false, + }); + + @override + State<_RowLeading> createState() => _RowLeadingState(); +} + +class _RowLeadingState extends State<_RowLeading> { + late final PopoverController popoverController; + + @override + void initState() { + super.initState(); + popoverController = PopoverController(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + triggerActions: PopoverTriggerFlags.none, + constraints: BoxConstraints.loose(const Size(140, 200)), + direction: PopoverDirection.rightWithCenterAligned, + margin: const EdgeInsets.all(6), + popupBuilder: (BuildContext popoverContext) { + final bloc = context.read(); + return RowActions( + viewId: bloc.viewId, + rowId: bloc.rowId, + ); + }, + child: Consumer( + builder: (context, state, _) { + return SizedBox( + width: GridSize.leadingHeaderPadding, + child: state.onEnter ? _activeWidget() : null, + ); + }, + ), + ); + } + + Widget _activeWidget() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + tooltipText: LocaleKeys.tooltip_addNewRow.tr(), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + width: 20, + height: 30, + onPressed: () => context.read().add(const RowEvent.createRow()), + iconPadding: const EdgeInsets.all(3), + icon: svgWidget( + 'home/add', + color: Theme.of(context).colorScheme.tertiary, + ), + ); + } +} + +class RowMenuButton extends StatefulWidget { + final VoidCallback openMenu; + final bool isDragEnabled; + + const RowMenuButton({ + required this.openMenu, + this.isDragEnabled = false, + super.key, + }); + + @override + State createState() => _RowMenuButtonState(); +} + +class _RowMenuButtonState extends State { + @override + Widget build(BuildContext context) { + return FlowyIconButton( + 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, + onPressed: () => widget.openMenu(), + iconPadding: const EdgeInsets.all(3), + icon: svgWidget( + 'editor/details', + color: Theme.of(context).colorScheme.tertiary, + ), + ); + } +} + +class RowContent extends StatelessWidget { + final VoidCallback onExpand; + final GridCellBuilder builder; + const RowContent({ + required this.builder, + required this.onExpand, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + !listEquals(previous.cells, current.cells), + builder: (context, state) { + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _makeCells(context, state.cellByFieldId), + ), + ); + }, + ); + } + + List _makeCells( + BuildContext context, + CellContextByFieldId cellByFieldId, + ) { + return cellByFieldId.values.map( + (cellId) { + final GridCellWidget child = builder.build(cellId); + + return CellContainer( + width: cellId.fieldInfo.width.toDouble(), + isPrimary: cellId.fieldInfo.isPrimary, + cellContainerNotifier: CellContainerNotifier(child), + accessoryBuilder: (buildContext) { + final builder = child.accessoryBuilder; + final List accessories = []; + if (cellId.fieldInfo.isPrimary) { + accessories.add( + GridCellAccessoryBuilder( + builder: (key) => PrimaryCellAccessory( + key: key, + onTapCallback: onExpand, + isCellEditing: buildContext.isCellEditing, + ), + ), + ); + } + + if (builder != null) { + accessories.addAll(builder(buildContext)); + } + + return accessories; + }, + child: child, + ); + }, + ).toList(); + } +} + +class RegionStateNotifier extends ChangeNotifier { + bool _onEnter = false; + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} + +class _RowEnterRegion extends StatefulWidget { + final Widget child; + const _RowEnterRegion({required this.child, Key? key}) : super(key: key); + + @override + State<_RowEnterRegion> createState() => _RowEnterRegionState(); +} + +class _RowEnterRegionState extends State<_RowEnterRegion> { + late final RegionStateNotifier _rowStateNotifier; + + @override + void initState() { + super.initState(); + _rowStateNotifier = RegionStateNotifier(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _rowStateNotifier, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => _rowStateNotifier.onEnter = true, + onExit: (p) => _rowStateNotifier.onEnter = false, + child: widget.child, + ), + ); + } + + @override + Future dispose() async { + _rowStateNotifier.dispose(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/shortcuts.dart new file mode 100644 index 0000000000..1e38c5647d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/shortcuts.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class GridShortcuts extends StatelessWidget { + final Widget child; + const GridShortcuts({required this.child, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Shortcuts( + shortcuts: bindKeys([]), + child: Actions( + dispatcher: LoggingActionDispatcher(), + actions: const {}, + child: child, + ), + ); + } +} + +Map bindKeys(List keys) { + return {for (var key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)}; +} + +Map> bindActions() { + return { + KeyboardKeyIdent: KeyboardBindingAction(), + }; +} + +class KeyboardKeyIdent extends Intent { + final KeyboardKey key; + + const KeyboardKeyIdent(this.key); +} + +class KeyboardBindingAction extends Action { + KeyboardBindingAction(); + + @override + void invoke(covariant KeyboardKeyIdent intent) { + // print(intent); + } +} + +class LoggingActionDispatcher extends ActionDispatcher { + @override + Object? invokeAction( + covariant Action action, + covariant Intent intent, [ + BuildContext? context, + ]) { + // print('Action invoked: $action($intent) from $context'); + super.invokeAction(action, intent, context); + + return null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart new file mode 100644 index 0000000000..b9c6d2468a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart @@ -0,0 +1,173 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/sort/sort_create_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class GridCreateSortList extends StatefulWidget { + final String viewId; + final FieldController fieldController; + final VoidCallback onClosed; + final VoidCallback? onCreateSort; + + const GridCreateSortList({ + required this.viewId, + required this.fieldController, + required this.onClosed, + this.onCreateSort, + Key? key, + }) : super(key: key); + + @override + State createState() => _GridCreateSortListState(); +} + +class _GridCreateSortListState extends State { + late CreateSortBloc editBloc; + + @override + void initState() { + editBloc = CreateSortBloc( + viewId: widget.viewId, + fieldController: widget.fieldController, + )..add(const CreateSortEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: editBloc, + child: BlocListener( + listener: (context, state) { + if (state.didCreateSort) { + widget.onClosed(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cells = state.creatableFields.map((fieldInfo) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: GridSortPropertyCell( + fieldInfo: fieldInfo, + onTap: (fieldInfo) => createSort(fieldInfo), + ), + ); + }).toList(); + + final List slivers = [ + SliverPersistentHeader( + pinned: true, + delegate: _SortTextFieldDelegate(), + ), + SliverToBoxAdapter( + child: ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + separatorBuilder: (BuildContext context, int index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + ), + ), + ]; + return CustomScrollView( + shrinkWrap: true, + slivers: slivers, + controller: ScrollController(), + physics: StyledScrollPhysics(), + ); + }, + ), + ), + ); + } + + @override + Future dispose() async { + editBloc.close(); + super.dispose(); + } + + void createSort(FieldInfo field) { + editBloc.add(CreateSortEvent.createDefaultSort(field)); + widget.onCreateSort?.call(); + } +} + +class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate { + _SortTextFieldDelegate(); + + double fixHeight = 46; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Container( + padding: const EdgeInsets.only(top: 4), + height: fixHeight, + child: FlowyTextField( + hintText: LocaleKeys.grid_settings_sortBy.tr(), + onChanged: (text) { + context + .read() + .add(CreateSortEvent.didReceiveFilterText(text)); + }, + ), + ); + } + + @override + double get maxExtent => fixHeight; + + @override + double get minExtent => fixHeight; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return false; + } +} + +class GridSortPropertyCell extends StatelessWidget { + final FieldInfo fieldInfo; + final Function(FieldInfo) onTap; + const GridSortPropertyCell({ + required this.fieldInfo, + required this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + fieldInfo.name, + color: AFThemeExtension.of(context).textColor, + ), + onTap: () => onTap(fieldInfo), + leftIcon: svgWidget( + fieldInfo.fieldType.iconName(), + color: Theme.of(context).iconTheme.color, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart new file mode 100644 index 0000000000..699d4448b3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart @@ -0,0 +1,53 @@ +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.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 OrderPanel extends StatelessWidget { + final Function(SortConditionPB) onCondition; + const OrderPanel({required this.onCondition, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final List children = SortConditionPB.values.map((condition) { + return OrderPannelItem( + condition: condition, + onCondition: onCondition, + ); + }).toList(); + + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 160), + child: IntrinsicWidth( + child: IntrinsicHeight( + child: Column( + children: children, + ), + ), + ), + ); + } +} + +class OrderPannelItem extends StatelessWidget { + final SortConditionPB condition; + final Function(SortConditionPB) onCondition; + const OrderPannelItem({ + required this.condition, + required this.onCondition, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(condition.title), + onTap: () => onCondition(condition), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_choice_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_choice_button.dart new file mode 100644 index 0000000000..40afc7019f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_choice_button.dart @@ -0,0 +1,54 @@ +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'; + +class SortChoiceButton extends StatelessWidget { + final String text; + final VoidCallback? onTap; + final Widget? leftIcon; + final Widget? rightIcon; + final Radius radius; + final bool editable; + + const SortChoiceButton({ + required this.text, + this.onTap, + this.radius = const Radius.circular(14), + this.leftIcon, + this.rightIcon, + this.editable = true, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final borderSide = BorderSide( + color: AFThemeExtension.of(context).toggleOffFill, + width: 1.0, + ); + + final decoration = BoxDecoration( + color: Colors.transparent, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(14)), + ); + + return FlowyButton( + decoration: decoration, + useIntrinsicWidth: true, + text: FlowyText( + text, + color: AFThemeExtension.of(context).textColor, + ), + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + radius: BorderRadius.all(radius), + leftIcon: leftIcon, + rightIcon: rightIcon, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: onTap, + disable: !editable, + disableOpacity: 1.0, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart new file mode 100644 index 0000000000..99fdb37146 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart @@ -0,0 +1,281 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/sort/sort_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/application/sort/util.dart'; +import 'package:appflowy/plugins/database_view/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/image.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 'dart:math' as math; + +import 'create_sort_list.dart'; +import 'order_panel.dart'; +import 'sort_choice_button.dart'; +import 'sort_info.dart'; + +class SortEditor extends StatefulWidget { + final String viewId; + final List sortInfos; + final FieldController fieldController; + const SortEditor({ + required this.viewId, + required this.fieldController, + required this.sortInfos, + Key? key, + }) : super(key: key); + + @override + State createState() => _SortEditorState(); +} + +class _SortEditorState extends State { + final popoverMutex = PopoverMutex(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SortEditorBloc( + viewId: widget.viewId, + fieldController: widget.fieldController, + sortInfos: widget.sortInfos, + )..add(const SortEditorEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return IntrinsicWidth( + child: IntrinsicHeight( + child: Column( + children: [ + _SortList(popoverMutex: popoverMutex), + DatabaseAddSortButton( + viewId: widget.viewId, + fieldController: widget.fieldController, + popoverMutex: popoverMutex, + ), + DatabaseDeleteSortButton(popoverMutex: popoverMutex), + ], + ), + ), + ); + }, + ), + ); + } +} + +class _SortList extends StatelessWidget { + final PopoverMutex popoverMutex; + const _SortList({required this.popoverMutex, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final List children = state.sortInfos + .map( + (info) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: DatabaseSortItem( + sortInfo: info, + popoverMutex: popoverMutex, + ), + ), + ) + .toList(); + + return Column( + children: children, + ); + }, + ); + } +} + +class DatabaseSortItem extends StatelessWidget { + final SortInfo sortInfo; + final PopoverMutex popoverMutex; + const DatabaseSortItem({ + required this.popoverMutex, + required this.sortInfo, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final nameButton = SortChoiceButton( + text: sortInfo.fieldInfo.name, + editable: false, + onTap: () {}, + ); + final orderButton = DatabaseSortItemOrderButton( + sortInfo: sortInfo, + popoverMutex: popoverMutex, + ); + + final deleteButton = FlowyIconButton( + width: 26, + onPressed: () { + context + .read() + .add(SortEditorEvent.deleteSort(sortInfo)); + }, + iconPadding: const EdgeInsets.all(5), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + icon: svgWidget( + "home/close", + color: Theme.of(context).iconTheme.color, + ), + ); + + return Row( + children: [ + SizedBox(height: 26, child: nameButton), + const HSpace(6), + SizedBox(height: 26, child: orderButton), + const HSpace(16), + deleteButton + ], + ); + } +} + +extension SortConditionExtension on SortConditionPB { + String get title { + switch (this) { + case SortConditionPB.Ascending: + return LocaleKeys.grid_sort_ascending.tr(); + case SortConditionPB.Descending: + return LocaleKeys.grid_sort_descending.tr(); + } + return ""; + } +} + +class DatabaseAddSortButton extends StatefulWidget { + final String viewId; + final FieldController fieldController; + final PopoverMutex popoverMutex; + const DatabaseAddSortButton({ + required this.viewId, + required this.fieldController, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + State createState() => _DatabaseAddSortButtonState(); +} + +class _DatabaseAddSortButtonState extends State { + final _popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: _popoverController, + mutex: widget.popoverMutex, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(200, 300)), + offset: const Offset(0, 8), + triggerActions: PopoverTriggerFlags.none, + asBarrier: true, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).greyHover, + disable: getCreatableSorts(widget.fieldController.fieldInfos).isEmpty, + text: FlowyText.medium(LocaleKeys.grid_sort_addSort.tr()), + onTap: () => _popoverController.show(), + leftIcon: const FlowySvg(name: 'home/add'), + ), + ), + popupBuilder: (BuildContext context) { + return GridCreateSortList( + viewId: widget.viewId, + fieldController: widget.fieldController, + onClosed: () => _popoverController.close(), + ); + }, + ); + } +} + +class DatabaseDeleteSortButton extends StatelessWidget { + final PopoverMutex popoverMutex; + const DatabaseDeleteSortButton({required this.popoverMutex, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(LocaleKeys.grid_sort_deleteSort.tr()), + onTap: () { + context + .read() + .add(const SortEditorEvent.deleteAllSorts()); + }, + leftIcon: const FlowySvg(name: 'editor/delete'), + ), + ); + }, + ); + } +} + +class DatabaseSortItemOrderButton extends StatefulWidget { + final SortInfo sortInfo; + final PopoverMutex popoverMutex; + const DatabaseSortItemOrderButton({ + required this.popoverMutex, + required this.sortInfo, + Key? key, + }) : super(key: key); + + @override + State createState() => + _DatabaseSortItemOrderButtonState(); +} + +class _DatabaseSortItemOrderButtonState + extends State { + final PopoverController popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + final arrow = Transform.rotate( + angle: -math.pi / 2, + child: svgWidget("home/arrow_left"), + ); + + return AppFlowyPopover( + controller: popoverController, + mutex: widget.popoverMutex, + constraints: BoxConstraints.loose(const Size(340, 200)), + direction: PopoverDirection.bottomWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + popupBuilder: (BuildContext popoverContext) { + return OrderPanel( + onCondition: (condition) { + context + .read() + .add(SortEditorEvent.setCondition(widget.sortInfo, condition)); + popoverController.close(); + }, + ); + }, + child: SortChoiceButton( + text: widget.sortInfo.sortPB.condition.title, + rightIcon: arrow, + onTap: () => popoverController.show(), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart new file mode 100644 index 0000000000..ff19e8794b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart @@ -0,0 +1,11 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; + +class SortInfo { + final SortPB sortPB; + final FieldInfo fieldInfo; + + SortInfo({required this.sortPB, required this.fieldInfo}); + + String get sortId => sortPB.id; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart new file mode 100644 index 0000000000..3ebebcac00 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart @@ -0,0 +1,91 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_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'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'dart:math' as math; + +import 'sort_choice_button.dart'; +import 'sort_editor.dart'; +import 'sort_info.dart'; + +class SortMenu extends StatelessWidget { + final FieldController fieldController; + const SortMenu({ + required this.fieldController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SortMenuBloc( + viewId: fieldController.viewId, + fieldController: fieldController, + )..add(const SortMenuEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + if (state.sortInfos.isNotEmpty) { + return AppFlowyPopover( + controller: PopoverController(), + constraints: BoxConstraints.loose(const Size(340, 200)), + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext popoverContext) { + return SortEditor( + viewId: state.viewId, + fieldController: context.read().fieldController, + sortInfos: state.sortInfos, + ); + }, + child: SortChoiceChip(sortInfos: state.sortInfos), + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } +} + +class SortChoiceChip extends StatelessWidget { + final List sortInfos; + final VoidCallback? onTap; + + const SortChoiceChip({ + Key? key, + required this.sortInfos, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final arrow = Transform.rotate( + angle: -math.pi / 2, + child: svgWidget( + "home/arrow_left", + color: Theme.of(context).iconTheme.color, + ), + ); + + final text = LocaleKeys.grid_settings_sort.tr(); + final leftIcon = svgWidget( + "grid/setting/sort", + color: Theme.of(context).iconTheme.color, + ); + + return SizedBox( + height: 28, + child: SortChoiceButton( + text: text, + leftIcon: leftIcon, + rightIcon: arrow, + onTap: onTap, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart new file mode 100644 index 0000000000..8d32ff981f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart @@ -0,0 +1,79 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/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/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({Key? key}) : super(key: key); + + @override + State createState() => _FilterButtonState(); +} + +class _FilterButtonState extends State { + final _popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final textColor = state.filters.isEmpty + ? AFThemeExtension.of(context).textColor + : Theme.of(context).colorScheme.primary; + + return _wrapPopover( + context, + SizedBox( + height: 26, + child: FlowyTextButton( + LocaleKeys.grid_settings_filter.tr(), + fontColor: textColor, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.typeOptionContentInsets, + onPressed: () { + final bloc = context.read(); + if (bloc.state.filters.isEmpty) { + _popoverController.show(); + } else { + bloc.add(const GridFilterMenuEvent.toggleMenu()); + } + }, + ), + ), + ); + }, + ); + } + + Widget _wrapPopover(BuildContext buildContext, Widget child) { + return AppFlowyPopover( + controller: _popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(200, 300)), + offset: const Offset(0, 8), + triggerActions: PopoverTriggerFlags.none, + child: child, + 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 GridFilterMenuEvent.toggleMenu()); + } + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart new file mode 100644 index 0000000000..04786deeea --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart @@ -0,0 +1,104 @@ +import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:flowy_infra/image.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/sizes.dart'; + +class DatabaseLayoutList extends StatefulWidget { + final String viewId; + final DatabaseLayoutPB currentLayout; + const DatabaseLayoutList({ + required this.viewId, + required this.currentLayout, + Key? key, + }) : super(key: key); + + @override + State createState() => _DatabaseLayoutListState(); +} + +class _DatabaseLayoutListState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DatabaseLayoutBloc( + viewId: widget.viewId, + databaseLayout: widget.currentLayout, + )..add(const DatabaseLayoutEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final cells = DatabaseLayoutPB.values.map((layout) { + final isSelected = state.databaseLayout == layout; + return DatabaseViewLayoutCell( + databaseLayout: layout, + isSelected: isSelected, + onTap: (selectedLayout) { + context + .read() + .add(DatabaseLayoutEvent.updateLayout(selectedLayout)); + }, + ); + }).toList(); + + return ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) => cells[index], + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + padding: const EdgeInsets.symmetric(vertical: 6.0), + ); + }, + ), + ); + } +} + +class DatabaseViewLayoutCell extends StatelessWidget { + final bool isSelected; + final DatabaseLayoutPB databaseLayout; + final void Function(DatabaseLayoutPB) onTap; + const DatabaseViewLayoutCell({ + required this.databaseLayout, + required this.isSelected, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + Widget? checkmark; + if (isSelected) { + checkmark = svgWidget("grid/checkmark"); + } + + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + databaseLayout.layoutName(), + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: svgWidget( + databaseLayout.iconName(), + color: Theme.of(context).iconTheme.color, + ), + rightIcon: checkmark, + onTap: () => onTap(databaseLayout), + ).padding(horizontal: 6.0), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart new file mode 100644 index 0000000000..cb093eae34 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart @@ -0,0 +1,68 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'filter_button.dart'; +import 'sort_button.dart'; + +class GridSettingBar extends StatelessWidget { + final DatabaseController controller; + final ToggleExtensionNotifier toggleExtension; + const GridSettingBar({ + required this.controller, + required this.toggleExtension, + super.key, + }); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => GridFilterMenuBloc( + viewId: controller.viewId, + fieldController: controller.fieldController, + )..add(const GridFilterMenuEvent.initial()), + ), + BlocProvider( + create: (context) => SortMenuBloc( + viewId: controller.viewId, + fieldController: controller.fieldController, + )..add(const SortMenuEvent.initial()), + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + ), + BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + ), + ], + child: SizedBox( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: GridSize.leadingHeaderPadding), + const Spacer(), + const FilterButton(), + const SortButton(), + SettingButton( + databaseController: controller, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart new file mode 100644 index 0000000000..c86982db15 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/application/sort/sort_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/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; + +import '../sort/create_sort_list.dart'; + +class SortButton extends StatefulWidget { + const SortButton({Key? key}) : super(key: key); + + @override + State createState() => _SortButtonState(); +} + +class _SortButtonState extends State { + final _popoverController = PopoverController(); + + @override + 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( + context, + SizedBox( + height: 26, + child: FlowyTextButton( + LocaleKeys.grid_settings_sort.tr(), + fontColor: textColor, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.typeOptionContentInsets, + onPressed: () { + final bloc = context.read(); + if (bloc.state.sortInfos.isEmpty) { + _popoverController.show(); + } else { + bloc.add(const SortMenuEvent.toggleMenu()); + } + }, + ), + ), + ); + }, + ); + } + + Widget wrapPopover(BuildContext buildContext, Widget child) { + return AppFlowyPopover( + controller: _popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(200, 300)), + offset: const Offset(0, 8), + margin: const EdgeInsets.all(6), + triggerActions: PopoverTriggerFlags.none, + child: child, + popupBuilder: (BuildContext context) { + final bloc = buildContext.read(); + return GridCreateSortList( + viewId: bloc.viewId, + fieldController: bloc.fieldController, + onClosed: () => _popoverController.close(), + onCreateSort: () { + if (!bloc.state.isVisible) { + bloc.add(const SortMenuEvent.toggleMenu()); + } + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart new file mode 100644 index 0000000000..0defa34026 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart @@ -0,0 +1,98 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:flowy_infra/theme_extension.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 '../application/field/field_controller.dart'; +import '../grid/presentation/layout/sizes.dart'; +import '../grid/presentation/widgets/filter/filter_menu.dart'; +import '../grid/presentation/widgets/sort/sort_menu.dart'; + +class DatabaseViewSettingExtension extends StatelessWidget { + final String viewId; + final DatabaseController databaseController; + final ToggleExtensionNotifier toggleExtension; + const DatabaseViewSettingExtension({ + required this.viewId, + required this.databaseController, + required this.toggleExtension, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: toggleExtension, + child: Consumer( + builder: (context, value, child) { + if (value.isToggled) { + return BlocProvider( + create: (context) => + DatabaseViewSettingExtensionBloc(viewId: viewId), + child: _DatabaseViewSettingContent( + fieldController: databaseController.fieldController, + ), + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } +} + +class _DatabaseViewSettingContent extends StatelessWidget { + final FieldController fieldController; + const _DatabaseViewSettingContent({ + required this.fieldController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final children = [ + Divider( + height: 1.0, + color: AFThemeExtension.of(context).toggleOffFill, + ), + const VSpace(6), + IntrinsicHeight( + child: Row( + children: [ + SortMenu( + fieldController: fieldController, + ), + const HSpace(6), + FilterMenu( + fieldController: fieldController, + ), + ], + ), + ) + ]; + + return _wrapPadding( + Column(children: children), + ); + }, + ); + } + + Widget _wrapPadding(Widget child) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.leadingHeaderPadding, + vertical: 6, + ), + child: child, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart new file mode 100644 index 0000000000..4a5e6a0cb2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart @@ -0,0 +1,415 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.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/presentation/home/home_stack.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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 '../application/database_controller.dart'; +import '../grid/presentation/layout/sizes.dart'; +import 'tar_bar_add_button.dart'; + +abstract class DatabaseTabBarItemBuilder { + const DatabaseTabBarItemBuilder(); + + /// Returns the content of the tab bar item. The content is shown when the tab + /// bar item is selected. It can be any kind of database view. + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ); + + /// Returns the setting bar of the tab bar item. The setting bar is shown on the + /// top right conner when the tab bar item is selected. + Widget settingBar( + BuildContext context, + DatabaseController controller, + ); + + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ); +} + +class DatabaseTabBarView extends StatefulWidget { + final ViewPB view; + const DatabaseTabBarView({ + required this.view, + super.key, + }); + + @override + State createState() => _DatabaseTabBarViewState(); +} + +class _DatabaseTabBarViewState extends State { + PageController? _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController( + initialPage: 0, + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => GridTabBarBloc(view: widget.view) + ..add( + const GridTabBarEvent.initial(), + ), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.selectedIndex != c.selectedIndex, + listener: (context, state) { + _pageController?.animateToPage( + state.selectedIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.ease, + ); + }, + ), + ], + child: Column( + children: [ + Row( + children: [ + BlocBuilder( + builder: (context, state) { + return const Flexible( + child: Padding( + padding: EdgeInsets.only(left: 50), + child: DatabaseTabBar(), + ), + ); + }, + ), + BlocBuilder( + builder: (context, state) { + return SizedBox( + width: 300, + child: Padding( + padding: const EdgeInsets.only(right: 50), + child: pageSettingBarFromState(state), + ), + ); + }, + ), + ], + ), + BlocBuilder( + builder: (context, state) { + return pageSettingBarExtensionFromState(state); + }, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return PageView( + pageSnapping: false, + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + children: pageContentFromState(state), + ); + }, + ), + ), + ], + ), + ), + ); + } + + List pageContentFromState(GridTabBarState state) { + return state.tabBars.map((tabBar) { + final controller = + state.tabBarControllerByViewId[tabBar.viewId]!.controller; + return tabBar.builder.content( + context, + tabBar.view, + controller, + ); + }).toList(); + } + + Widget pageSettingBarFromState(GridTabBarState state) { + if (state.tabBars.length < state.selectedIndex) { + return const SizedBox.shrink(); + } + final tarBar = state.tabBars[state.selectedIndex]; + final controller = + state.tabBarControllerByViewId[tarBar.viewId]!.controller; + return tarBar.builder.settingBar( + context, + controller, + ); + } + + Widget pageSettingBarExtensionFromState(GridTabBarState state) { + if (state.tabBars.length < state.selectedIndex) { + return const SizedBox.shrink(); + } + final tarBar = state.tabBars[state.selectedIndex]; + final controller = + state.tabBarControllerByViewId[tarBar.viewId]!.controller; + return tarBar.builder.settingBarExtension( + context, + controller, + ); + } +} + +class DatabaseTabBarViewPlugin extends Plugin { + @override + final ViewPluginNotifier notifier; + final PluginType _pluginType; + + DatabaseTabBarViewPlugin({ + required ViewPB view, + required PluginType pluginType, + }) : _pluginType = pluginType, + notifier = ViewPluginNotifier(view: view); + + @override + PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder( + notifier: notifier, + ); + + @override + PluginId get id => notifier.view.id; + + @override + PluginType get pluginType => _pluginType; +} + +class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { + final ViewPluginNotifier notifier; + + DatabasePluginWidgetBuilder({ + required this.notifier, + Key? key, + }); + + @override + Widget get leftBarItem => ViewLeftBarItem(view: notifier.view); + + @override + Widget buildWidget({PluginContext? context}) { + notifier.isDeleted.addListener(() { + notifier.isDeleted.value.fold(() => null, (deletedView) { + if (deletedView.hasIndex()) { + context?.onDeleted(notifier.view, deletedView.index); + } + }); + }); + return DatabaseTabBarView( + key: ValueKey(notifier.view.id), + view: notifier.view, + ); + } + + @override + List get navigationItems => [this]; +} + +class DatabaseTabBar extends StatefulWidget { + const DatabaseTabBar({super.key}); + + @override + State createState() => _DatabaseTabBarState(); +} + +class _DatabaseTabBarState extends State { + final _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + 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( + GridTabBarEvent.selectView(selectedView.id), + ); + }, + ); + }).toList(); + + return Row( + children: [ + Flexible( + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + child: IntrinsicWidth( + child: Row(children: children), + ), + ), + ), + AddDatabaseViewButton( + onTap: (action) async { + context.read().add( + GridTabBarEvent.createView(action), + ); + }, + ), + ], + ); + }, + ); + } +} + +class DatabaseTabBarItem extends StatelessWidget { + final bool isSelected; + final ViewPB view; + final Function(ViewPB) onTap; + const DatabaseTabBarItem({ + required this.view, + required this.isSelected, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 80, maxWidth: 160), + child: IntrinsicWidth( + child: Column( + children: [ + TabBarItemButton( + view: view, + onTap: () => onTap(view), + ), + if (isSelected) + Divider( + height: 1, + thickness: 2, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ); + } +} + +class TabBarItemButton extends StatelessWidget { + final ViewPB view; + final VoidCallback onTap; + const TabBarItemButton({ + required this.view, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + actions: TabBarViewAction.values, + buildChild: (controller) { + return FlowyButton( + radius: Corners.s5Border, + hoverColor: AFThemeExtension.of(context).greyHover, + onTap: onTap, + onSecondaryTap: () { + controller.show(); + }, + text: FlowyText.medium( + view.name, + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + margin: GridSize.cellContentInsets, + leftIcon: svgWidget( + view.iconName, + color: Theme.of(context).iconTheme.color, + ), + ); + }, + onSelected: (action, controller) { + switch (action) { + case TabBarViewAction.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.menuAppHeader_renameDialog.tr(), + value: view.name, + confirm: (newValue) { + context.read().add( + GridTabBarEvent.renameView(view.id, newValue), + ); + }, + ).show(context); + break; + case TabBarViewAction.delete: + NavigatorAlertDialog( + title: LocaleKeys.grid_deleteView.tr(), + confirm: () { + context.read().add( + GridTabBarEvent.deleteView(view.id), + ); + }, + ).show(context); + + break; + } + controller.close(); + }, + ); + } +} + +enum TabBarViewAction implements ActionCell { + rename, + delete; + + @override + String get name { + switch (this) { + case TabBarViewAction.rename: + return LocaleKeys.disclosureAction_rename.tr(); + case TabBarViewAction.delete: + return LocaleKeys.disclosureAction_delete.tr(); + } + } + + Widget icon(Color iconColor) { + switch (this) { + case TabBarViewAction.rename: + return const FlowySvg(name: 'editor/edit'); + case TabBarViewAction.delete: + return const FlowySvg(name: 'editor/delete'); + } + } + + @override + Widget? leftIcon(Color iconColor) => icon(iconColor); + + @override + Widget? rightIcon(Color iconColor) => null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart new file mode 100644 index 0000000000..10d13878c7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart @@ -0,0 +1,156 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/extension.dart'; +import 'package:flutter/material.dart'; + +class AddDatabaseViewButton extends StatefulWidget { + final Function(AddButtonAction) onTap; + const AddDatabaseViewButton({ + required this.onTap, + super.key, + }); + + @override + State createState() => _AddDatabaseViewButtonState(); +} + +class _AddDatabaseViewButtonState extends State { + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + constraints: BoxConstraints.loose(const Size(200, 400)), + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 8), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + child: FlowyIconButton( + iconPadding: const EdgeInsets.all(4), + hoverColor: AFThemeExtension.of(context).greyHover, + onPressed: () => popoverController.show(), + icon: svgWidget( + 'home/add', + color: Theme.of(context).colorScheme.tertiary, + ), + ), + popupBuilder: (BuildContext context) { + return TarBarAddButtonAction( + onTap: (action) { + popoverController.close(); + widget.onTap(action); + }, + ); + }, + ); + } +} + +class TarBarAddButtonAction extends StatelessWidget { + final Function(AddButtonAction) onTap; + const TarBarAddButtonAction({ + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + final cells = AddButtonAction.values.map((layout) { + return TarBarAddButtonActionCell( + action: layout, + onTap: onTap, + ); + }).toList(); + + return ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) => cells[index], + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + padding: const EdgeInsets.symmetric(vertical: 6.0), + ); + } +} + +class TarBarAddButtonActionCell extends StatelessWidget { + final AddButtonAction action; + final void Function(AddButtonAction) onTap; + const TarBarAddButtonActionCell({ + required this.action, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + '${LocaleKeys.grid_createView.tr()} ${action.title}', + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: svgWidget( + action.iconName, + color: Theme.of(context).iconTheme.color, + ), + onTap: () => onTap(action), + ).padding(horizontal: 6.0), + ); + } +} + +enum AddButtonAction { + grid, + calendar, + board; + + String get title { + switch (this) { + case AddButtonAction.board: + return LocaleKeys.board_menuName.tr(); + case AddButtonAction.calendar: + return LocaleKeys.calendar_menuName.tr(); + case AddButtonAction.grid: + return LocaleKeys.grid_menuName.tr(); + default: + return ""; + } + } + + ViewLayoutPB get layoutType { + switch (this) { + case AddButtonAction.board: + return ViewLayoutPB.Board; + case AddButtonAction.calendar: + return ViewLayoutPB.Calendar; + case AddButtonAction.grid: + return ViewLayoutPB.Grid; + default: + return ViewLayoutPB.Grid; + } + } + + String get iconName { + switch (this) { + case AddButtonAction.board: + return 'editor/board'; + case AddButtonAction.calendar: + return "editor/grid"; + case AddButtonAction.grid: + return "editor/grid"; + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart new file mode 100644 index 0000000000..3cf0ff329d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart @@ -0,0 +1,77 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import '../../../application/cell/cell_controller_builder.dart'; + +part 'checkbox_card_cell_bloc.freezed.dart'; + +class CheckboxCardCellBloc + extends Bloc { + final CheckboxCellController cellController; + void Function()? _onCellChangedFn; + CheckboxCardCellBloc({ + required this.cellController, + }) : super(CheckboxCardCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith(isSelected: _isSelected(cellData))); + }, + select: () async { + cellController.saveCellData(!state.isSelected ? "Yes" : "No"); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(CheckboxCardCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class CheckboxCardCellEvent with _$CheckboxCardCellEvent { + const factory CheckboxCardCellEvent.initial() = _InitialCell; + const factory CheckboxCardCellEvent.select() = _Selected; + const factory CheckboxCardCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; +} + +@freezed +class CheckboxCardCellState with _$CheckboxCardCellState { + const factory CheckboxCardCellState({ + required bool isSelected, + }) = _CheckboxCellState; + + factory CheckboxCardCellState.initial(TextCellController context) { + return CheckboxCardCellState( + isSelected: _isSelected(context.getCellData()), + ); + } +} + +bool _isSelected(String? cellData) { + // The backend use "Yes" and "No" to represent the checkbox cell data. + return cellData == "Yes"; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart new file mode 100644 index 0000000000..d852892bf8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart @@ -0,0 +1,86 @@ +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 'dart:async'; + +import '../../../application/cell/cell_controller_builder.dart'; +import '../../../application/field/field_controller.dart'; +part 'date_card_cell_bloc.freezed.dart'; + +class DateCardCellBloc extends Bloc { + final DateCellController cellController; + void Function()? _onCellChangedFn; + + DateCardCellBloc({required this.cellController}) + : super(DateCardCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () => _startListening(), + didReceiveCellUpdate: (DateCellDataPB? cellData) { + emit( + state.copyWith( + data: cellData, + dateStr: _dateStrFromCellData(cellData), + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((data) { + if (!isClosed) { + add(DateCardCellEvent.didReceiveCellUpdate(data)); + } + }), + ); + } +} + +@freezed +class DateCardCellEvent with _$DateCardCellEvent { + const factory DateCardCellEvent.initial() = _InitialCell; + const factory DateCardCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = + _DidReceiveCellUpdate; +} + +@freezed +class DateCardCellState with _$DateCardCellState { + const factory DateCardCellState({ + required DateCellDataPB? data, + required String dateStr, + required FieldInfo fieldInfo, + }) = _DateCardCellState; + + factory DateCardCellState.initial(DateCellController context) { + final cellData = context.getCellData(); + + return DateCardCellState( + fieldInfo: context.fieldInfo, + data: cellData, + dateStr: _dateStrFromCellData(cellData), + ); + } +} + +String _dateStrFromCellData(DateCellDataPB? cellData) { + String dateStr = ""; + if (cellData != null) { + dateStr = "${cellData.date} ${cellData.time}"; + } + return dateStr; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/number_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/number_card_cell_bloc.dart new file mode 100644 index 0000000000..54c6d242d1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/number_card_cell_bloc.dart @@ -0,0 +1,68 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import '../../../application/cell/cell_controller_builder.dart'; + +part 'number_card_cell_bloc.freezed.dart'; + +class NumberCardCellBloc + extends Bloc { + final NumberCellController cellController; + void Function()? _onCellChangedFn; + NumberCardCellBloc({ + required this.cellController, + }) : super(NumberCardCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (content) { + emit(state.copyWith(content: content)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(NumberCardCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class NumberCardCellEvent with _$NumberCardCellEvent { + const factory NumberCardCellEvent.initial() = _InitialCell; + const factory NumberCardCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; +} + +@freezed +class NumberCardCellState with _$NumberCardCellState { + const factory NumberCardCellState({ + required String content, + }) = _NumberCardCellState; + + factory NumberCardCellState.initial(TextCellController context) => + NumberCardCellState( + content: context.getCellData() ?? "", + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart new file mode 100644 index 0000000000..08b72aa430 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'select_option_card_cell_bloc.freezed.dart'; + +class SelectOptionCardCellBloc + extends Bloc { + final SelectOptionCellController cellController; + void Function()? _onCellChangedFn; + + SelectOptionCardCellBloc({ + required this.cellController, + }) : super(SelectOptionCardCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveOptions: (List selectedOptions) { + emit(state.copyWith(selectedOptions: selectedOptions)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((selectOptionContext) { + if (!isClosed) { + add( + SelectOptionCardCellEvent.didReceiveOptions( + selectOptionContext?.selectOptions ?? [], + ), + ); + } + }), + ); + } +} + +@freezed +class SelectOptionCardCellEvent with _$SelectOptionCardCellEvent { + const factory SelectOptionCardCellEvent.initial() = _InitialCell; + const factory SelectOptionCardCellEvent.didReceiveOptions( + List selectedOptions, + ) = _DidReceiveOptions; +} + +@freezed +class SelectOptionCardCellState with _$SelectOptionCardCellState { + const factory SelectOptionCardCellState({ + required List selectedOptions, + }) = _SelectOptionCardCellState; + + factory SelectOptionCardCellState.initial( + SelectOptionCellController context, + ) { + final data = context.getCellData(); + return SelectOptionCardCellState( + selectedOptions: data?.selectOptions ?? [], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart new file mode 100644 index 0000000000..34266943b5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart @@ -0,0 +1,79 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'text_card_cell_bloc.freezed.dart'; + +class TextCardCellBloc extends Bloc { + final TextCellController cellController; + void Function()? _onCellChangedFn; + TextCardCellBloc({ + required this.cellController, + }) : super(TextCardCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (content) { + emit(state.copyWith(content: content)); + }, + updateText: (text) { + if (text != state.content) { + cellController.saveCellData(text); + emit(state.copyWith(content: text)); + } + }, + enableEdit: (bool enabled) { + emit(state.copyWith(enableEdit: enabled)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(TextCardCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class TextCardCellEvent with _$TextCardCellEvent { + const factory TextCardCellEvent.initial() = _InitialCell; + const factory TextCardCellEvent.updateText(String text) = _UpdateContent; + const factory TextCardCellEvent.enableEdit(bool enabled) = _EnableEdit; + const factory TextCardCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; +} + +@freezed +class TextCardCellState with _$TextCardCellState { + const factory TextCardCellState({ + required String content, + required bool enableEdit, + }) = _TextCardCellState; + + factory TextCardCellState.initial(TextCellController context) => + TextCardCellState( + content: context.getCellData() ?? "", + enableEdit: false, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart new file mode 100644 index 0000000000..6b3b084d1b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'url_card_cell_bloc.freezed.dart'; + +class URLCardCellBloc extends Bloc { + final URLCellController cellController; + void Function()? _onCellChangedFn; + URLCardCellBloc({ + required this.cellController, + }) : super(URLCardCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit( + state.copyWith( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ), + ); + }, + updateURL: (String url) { + cellController.saveCellData(url, deduplicate: true); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(URLCardCellEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class URLCardCellEvent with _$URLCardCellEvent { + const factory URLCardCellEvent.initial() = _InitialCell; + const factory URLCardCellEvent.updateURL(String url) = _UpdateURL; + const factory URLCardCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; +} + +@freezed +class URLCardCellState with _$URLCardCellState { + const factory URLCardCellState({ + required String content, + required String url, + }) = _URLCardCellState; + + factory URLCardCellState.initial(URLCellController context) { + final cellData = context.getCellData(); + return URLCardCellState( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/board_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/board_number_cell.dart new file mode 100644 index 0000000000..78a19bacdf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/board_number_cell.dart @@ -0,0 +1,68 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'bloc/number_card_cell_bloc.dart'; +import 'define.dart'; + +class BoardNumberCell extends StatefulWidget { + final String groupId; + final CellControllerBuilder cellControllerBuilder; + + const BoardNumberCell({ + required this.groupId, + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _NumberCardCellState(); +} + +class _NumberCardCellState extends State { + late NumberCardCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as NumberCellController; + + _cellBloc = NumberCardCellBloc(cellController: cellController) + ..add(const NumberCardCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: CardSizes.cardCellVPadding, + ), + child: FlowyText.medium( + state.content, + fontSize: 14, + ), + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart new file mode 100644 index 0000000000..db825e4fc5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart @@ -0,0 +1,327 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'card_bloc.dart'; +import 'cells/card_cell.dart'; +import 'card_cell_builder.dart'; +import 'container/accessory.dart'; +import 'container/card_container.dart'; + +/// Edit a database row with card style widget +class RowCard extends StatefulWidget { + final RowMetaPB rowMeta; + final String viewId; + final String? groupingFieldId; + + /// Allows passing a custom card data object to the card. The card will be + /// returned in the [CardCellBuilder] and can be used to build the card. + final CustomCardData? cardData; + final bool isEditing; + final RowCache rowCache; + + /// The [CardCellBuilder] is used to build the card cells. + final CardCellBuilder cellBuilder; + + /// Called when the user taps on the card. + final void Function(BuildContext) openCard; + + /// Called when the user starts editing the card. + final VoidCallback onStartEditing; + + /// Called when the user ends editing the card. + final VoidCallback onEndEditing; + + /// The [RowCardRenderHook] is used to render the card's cell. Other than + /// using the default cell builder. For example the [SelectOptionCardCell] + final RowCardRenderHook? renderHook; + + final RowCardStyleConfiguration styleConfiguration; + + const RowCard({ + required this.rowMeta, + required this.viewId, + this.groupingFieldId, + required this.isEditing, + required this.rowCache, + required this.cellBuilder, + required this.openCard, + required this.onStartEditing, + required this.onEndEditing, + this.cardData, + this.styleConfiguration = const RowCardStyleConfiguration( + showAccessory: true, + ), + this.renderHook, + Key? key, + }) : super(key: key); + + @override + State> createState() => + _RowCardState(); +} + +class _RowCardState extends State> { + late final CardBloc _cardBloc; + late final EditableRowNotifier rowNotifier; + late final PopoverController popoverController; + AccessoryType? accessoryType; + + @override + void initState() { + rowNotifier = EditableRowNotifier(isEditing: widget.isEditing); + _cardBloc = CardBloc( + viewId: widget.viewId, + groupFieldId: widget.groupingFieldId, + isEditing: widget.isEditing, + rowMeta: widget.rowMeta, + rowCache: widget.rowCache, + )..add(const RowCardEvent.initial()); + + rowNotifier.isEditing.addListener(() { + if (!mounted) return; + _cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value)); + + if (rowNotifier.isEditing.value) { + widget.onStartEditing(); + } else { + widget.onEndEditing(); + } + }); + + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cardBloc, + child: BlocBuilder( + buildWhen: (previous, current) { + // Rebuild when: + // 1.If the length of the cells is not the same + // 2.isEditing changed + if (previous.cells.length != current.cells.length || + previous.isEditing != current.isEditing) { + return true; + } + + // 3.Compare the content of the cells. The cells consists of + // list of [BoardCellEquatable] that extends the [Equatable]. + return !listEquals(previous.cells, current.cells); + }, + builder: (context, state) { + return AppFlowyPopover( + controller: popoverController, + triggerActions: PopoverTriggerFlags.none, + constraints: BoxConstraints.loose(const Size(140, 200)), + margin: const EdgeInsets.all(6), + direction: PopoverDirection.rightWithCenterAligned, + popupBuilder: (popoverContext) => _handlePopoverBuilder( + context, + popoverContext, + ), + child: RowCardContainer( + buildAccessoryWhen: () => state.isEditing == false, + accessoryBuilder: (context) { + if (widget.styleConfiguration.showAccessory == false) { + return []; + } else { + return [ + _CardEditOption(rowNotifier: rowNotifier), + _CardMoreOption(), + ]; + } + }, + openAccessory: _handleOpenAccessory, + openCard: (context) => widget.openCard(context), + child: _CardContent( + rowNotifier: rowNotifier, + cellBuilder: widget.cellBuilder, + styleConfiguration: widget.styleConfiguration, + cells: state.cells, + renderHook: widget.renderHook, + cardData: widget.cardData, + ), + ), + ); + }, + ), + ); + } + + void _handleOpenAccessory(AccessoryType newAccessoryType) { + accessoryType = newAccessoryType; + switch (newAccessoryType) { + case AccessoryType.edit: + break; + case AccessoryType.more: + popoverController.show(); + break; + } + } + + Widget _handlePopoverBuilder( + BuildContext context, + BuildContext popoverContext, + ) { + switch (accessoryType!) { + case AccessoryType.edit: + throw UnimplementedError(); + case AccessoryType.more: + return RowActions( + viewId: context.read().viewId, + rowId: context.read().rowMeta.id, + ); + } + } + + @override + Future dispose() async { + rowNotifier.dispose(); + _cardBloc.close(); + super.dispose(); + } +} + +class _CardContent extends StatelessWidget { + final CardCellBuilder cellBuilder; + final EditableRowNotifier rowNotifier; + final List cells; + final RowCardRenderHook? renderHook; + final CustomCardData? cardData; + final RowCardStyleConfiguration styleConfiguration; + const _CardContent({ + required this.rowNotifier, + required this.cellBuilder, + required this.cells, + required this.cardData, + required this.styleConfiguration, + this.renderHook, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (styleConfiguration.hoverStyle != null) { + return FlowyHover( + style: styleConfiguration.hoverStyle, + child: Padding( + padding: styleConfiguration.cardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: _makeCells(context, cells), + ), + ), + ); + } + return Padding( + padding: styleConfiguration.cardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: _makeCells(context, cells), + ), + ); + } + + List _makeCells( + BuildContext context, + List cells, + ) { + final List children = []; + // Remove all the cell listeners. + rowNotifier.unbind(); + + cells.asMap().forEach( + (int index, DatabaseCellContext cellContext) { + final isEditing = index == 0 ? rowNotifier.isEditing.value : false; + final cellNotifier = EditableCardNotifier(isEditing: isEditing); + + if (index == 0) { + // Only use the first cell to receive user's input when click the edit + // button + rowNotifier.bindCell(cellContext, cellNotifier); + } + + final child = Padding( + key: cellContext.key(), + padding: styleConfiguration.cellPadding, + child: cellBuilder.buildCell( + cellContext: cellContext, + cellNotifier: cellNotifier, + renderHook: renderHook, + cardData: cardData, + ), + ); + + children.add(child); + }, + ); + return children; + } +} + +class _CardMoreOption extends StatelessWidget with CardAccessory { + _CardMoreOption({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(3.0), + child: svgWidget( + 'grid/details', + color: Theme.of(context).iconTheme.color, + ), + ); + } + + @override + AccessoryType get type => AccessoryType.more; +} + +class _CardEditOption extends StatelessWidget with CardAccessory { + final EditableRowNotifier rowNotifier; + const _CardEditOption({ + required this.rowNotifier, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(3.0), + child: svgWidget( + 'editor/edit', + color: Theme.of(context).iconTheme.color, + ), + ); + } + + @override + void onTap(BuildContext context) => rowNotifier.becomeFirstResponder(); + + @override + AccessoryType get type => AccessoryType.edit; +} + +class RowCardStyleConfiguration { + final bool showAccessory; + final EdgeInsets cellPadding; + final EdgeInsets cardPadding; + final HoverStyle? hoverStyle; + + const RowCardStyleConfiguration({ + this.showAccessory = true, + this.cellPadding = const EdgeInsets.only(left: 4, right: 4), + this.cardPadding = const EdgeInsets.all(8), + this.hoverStyle, + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart new file mode 100644 index 0000000000..f2af6128c2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart @@ -0,0 +1,135 @@ +import 'dart:collection'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import '../../application/cell/cell_service.dart'; +import '../../application/row/row_cache.dart'; +import '../../application/row/row_service.dart'; + +part 'card_bloc.freezed.dart'; + +class CardBloc extends Bloc { + final RowMetaPB rowMeta; + final String? groupFieldId; + final RowBackendService _rowBackendSvc; + final RowCache _rowCache; + VoidCallback? _rowCallback; + final String viewId; + + CardBloc({ + required this.rowMeta, + required this.groupFieldId, + required this.viewId, + required RowCache rowCache, + required bool isEditing, + }) : _rowBackendSvc = RowBackendService(viewId: viewId), + _rowCache = rowCache, + super( + RowCardState.initial( + _makeCells(groupFieldId, rowCache.loadGridCells(rowMeta)), + isEditing, + ), + ) { + on( + (event, emit) async { + await event.when( + initial: () async { + await _startListening(); + }, + didReceiveCells: (cells, reason) async { + emit( + state.copyWith( + cells: cells, + changeReason: reason, + ), + ); + }, + setIsEditing: (bool isEditing) { + emit(state.copyWith(isEditing: isEditing)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_rowCallback != null) { + _rowCache.removeRowListener(_rowCallback!); + _rowCallback = null; + } + return super.close(); + } + + RowInfo rowInfo() { + return RowInfo( + viewId: _rowBackendSvc.viewId, + fields: UnmodifiableListView( + state.cells.map((cell) => cell.fieldInfo).toList(), + ), + rowId: rowMeta.id, + rowMeta: rowMeta, + ); + } + + Future _startListening() async { + _rowCallback = _rowCache.addListener( + rowId: rowMeta.id, + onCellUpdated: (cellMap, reason) { + if (!isClosed) { + final cells = _makeCells(groupFieldId, cellMap); + add(RowCardEvent.didReceiveCells(cells, reason)); + } + }, + ); + } +} + +List _makeCells( + String? groupFieldId, + CellContextByFieldId originalCellMap, +) { + final List cells = []; + for (final entry in originalCellMap.entries) { + // Filter out the cell if it's fieldId equal to the groupFieldId + if (groupFieldId != null) { + if (entry.value.fieldId == groupFieldId) { + continue; + } + } + + cells.add(entry.value); + } + return cells; +} + +@freezed +class RowCardEvent with _$RowCardEvent { + const factory RowCardEvent.initial() = _InitialRow; + const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing; + const factory RowCardEvent.didReceiveCells( + List cells, + RowsChangedReason reason, + ) = _DidReceiveCells; +} + +@freezed +class RowCardState with _$RowCardState { + const factory RowCardState({ + required List cells, + required bool isEditing, + RowsChangedReason? changeReason, + }) = _RowCardState; + + factory RowCardState.initial( + List cells, + bool isEditing, + ) => + RowCardState( + cells: cells, + isEditing: isEditing, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart new file mode 100644 index 0000000000..676e5e5cc4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -0,0 +1,94 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/material.dart'; + +import '../../application/cell/cell_service.dart'; +import 'cells/card_cell.dart'; +import 'cells/checkbox_card_cell.dart'; +import 'cells/checklist_card_cell.dart'; +import 'cells/date_card_cell.dart'; +import 'cells/number_card_cell.dart'; +import 'cells/select_option_card_cell.dart'; +import 'cells/text_card_cell.dart'; +import 'cells/url_card_cell.dart'; + +// T represents as the Generic card data +class CardCellBuilder { + final CellCache cellCache; + final Map? styles; + + CardCellBuilder(this.cellCache, {this.styles}); + + Widget buildCell({ + CustomCardData? cardData, + required DatabaseCellContext cellContext, + EditableCardNotifier? cellNotifier, + RowCardRenderHook? renderHook, + }) { + final cellControllerBuilder = CellControllerBuilder( + cellContext: cellContext, + cellCache: cellCache, + ); + + final key = cellContext.key(); + final style = styles?[cellContext.fieldType]; + switch (cellContext.fieldType) { + case FieldType.Checkbox: + return CheckboxCardCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateCardCell( + renderHook: renderHook?.renderHook[FieldType.DateTime], + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.SingleSelect: + return SelectOptionCardCell( + renderHook: renderHook?.renderHook[FieldType.SingleSelect], + cellControllerBuilder: cellControllerBuilder, + cardData: cardData, + key: key, + ); + case FieldType.MultiSelect: + return SelectOptionCardCell( + renderHook: renderHook?.renderHook[FieldType.MultiSelect], + cellControllerBuilder: cellControllerBuilder, + cardData: cardData, + editableNotifier: cellNotifier, + key: key, + ); + case FieldType.Checklist: + return ChecklistCardCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.Number: + return NumberCardCell( + renderHook: renderHook?.renderHook[FieldType.Number], + style: isStyleOrNull(style), + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.RichText: + return TextCardCell( + renderHook: renderHook?.renderHook[FieldType.RichText], + cellControllerBuilder: cellControllerBuilder, + editableNotifier: cellNotifier, + cardData: cardData, + style: isStyleOrNull(style), + key: key, + ); + case FieldType.URL: + return URLCardCell( + style: isStyleOrNull(style), + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + } + throw UnimplementedError; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart new file mode 100644 index 0000000000..db71945acd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart @@ -0,0 +1,179 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:flutter/material.dart'; + +typedef CellRenderHook = Widget? Function( + C cellData, + CustomCardData cardData, + BuildContext buildContext, +); +typedef RenderHookByFieldType = Map>; + +/// The [RowCardRenderHook] is used to customize the rendering of the +/// card cell. Each cell has itw own field type. So the [renderHook] +/// is a map of [FieldType] to [CellRenderHook]. +class RowCardRenderHook { + final RenderHookByFieldType renderHook = {}; + RowCardRenderHook(); + + /// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect + void addSelectOptionHook( + CellRenderHook, CustomCardData?> hook, + ) { + final hookFn = _typeSafeHook>(hook); + renderHook[FieldType.SingleSelect] = hookFn; + renderHook[FieldType.MultiSelect] = hookFn; + } + + /// Add a render hook for the [FieldType.RichText] + void addTextCellHook( + CellRenderHook hook, + ) { + renderHook[FieldType.RichText] = _typeSafeHook(hook); + } + + /// Add a render hook for the [FieldType.Number] + void addNumberCellHook( + CellRenderHook hook, + ) { + renderHook[FieldType.Number] = _typeSafeHook(hook); + } + + /// Add a render hook for the [FieldType.Date] + void addDateCellHook( + CellRenderHook hook, + ) { + renderHook[FieldType.DateTime] = _typeSafeHook(hook); + } + + CellRenderHook _typeSafeHook( + CellRenderHook hook, + ) { + hookFn(cellData, cardData, buildContext) { + if (cellData == null) { + return null; + } + + if (cellData is C) { + return hook(cellData, cardData, buildContext); + } else { + Log.debug("Unexpected cellData type: ${cellData.runtimeType}"); + return null; + } + } + + return hookFn; + } +} + +abstract class CardCellStyle {} + +S? isStyleOrNull(CardCellStyle? style) { + if (style is S) { + return style as S; + } else { + return null; + } +} + +abstract class CardCell extends StatefulWidget { + final T? cardData; + final S? style; + + const CardCell({super.key, this.cardData, this.style}); +} + +class EditableCardNotifier { + final ValueNotifier isCellEditing; + + EditableCardNotifier({bool isEditing = false}) + : isCellEditing = ValueNotifier(isEditing); + + void dispose() { + isCellEditing.dispose(); + } +} + +class EditableRowNotifier { + final Map _cells = {}; + final ValueNotifier isEditing; + + EditableRowNotifier({required bool isEditing}) + : isEditing = ValueNotifier(isEditing); + + void bindCell( + DatabaseCellContext cellIdentifier, + EditableCardNotifier notifier, + ) { + assert( + _cells.values.isEmpty, + 'Only one cell can receive the notification', + ); + final id = EditableCellId.from(cellIdentifier); + _cells[id]?.dispose(); + + notifier.isCellEditing.addListener(() { + isEditing.value = notifier.isCellEditing.value; + }); + + _cells[EditableCellId.from(cellIdentifier)] = notifier; + } + + void becomeFirstResponder() { + if (_cells.values.isEmpty) return; + assert( + _cells.values.length == 1, + 'Only one cell can receive the notification', + ); + _cells.values.first.isCellEditing.value = true; + } + + void resignFirstResponder() { + if (_cells.values.isEmpty) return; + assert( + _cells.values.length == 1, + 'Only one cell can receive the notification', + ); + _cells.values.first.isCellEditing.value = false; + } + + void unbind() { + for (final notifier in _cells.values) { + notifier.dispose(); + } + _cells.clear(); + } + + void dispose() { + for (final notifier in _cells.values) { + notifier.dispose(); + } + + _cells.clear(); + } +} + +abstract mixin class EditableCell { + // Each cell notifier will be bind to the [EditableRowNotifier], which enable + // the row notifier receive its cells event. For example: begin editing the + // cell or end editing the cell. + // + EditableCardNotifier? get editableNotifier; +} + +class EditableCellId { + String fieldId; + RowId rowId; + + EditableCellId(this.rowId, this.fieldId); + + factory EditableCellId.from(DatabaseCellContext cellIdentifier) => + EditableCellId( + cellIdentifier.rowId, + cellIdentifier.fieldId, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart new file mode 100644 index 0000000000..33fef71bf0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart @@ -0,0 +1,69 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../bloc/checkbox_card_cell_bloc.dart'; +import 'card_cell.dart'; + +class CheckboxCardCell extends CardCell { + final CellControllerBuilder cellControllerBuilder; + + const CheckboxCardCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _CheckboxCardCellState(); +} + +class _CheckboxCardCellState extends State { + late CheckboxCardCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as CheckboxCellController; + _cellBloc = CheckboxCardCellBloc(cellController: cellController); + _cellBloc.add(const CheckboxCardCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.isSelected != current.isSelected, + builder: (context, state) { + final icon = state.isSelected + ? svgWidget('editor/editor_check') + : svgWidget('editor/editor_uncheck'); + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + onPressed: () => context + .read() + .add(const CheckboxCardCellEvent.select()), + ), + ), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart new file mode 100644 index 0000000000..132a59744d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart @@ -0,0 +1,42 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../row/cells/checklist_cell/checklist_cell_bloc.dart'; +import 'card_cell.dart'; + +class ChecklistCardCell extends CardCell { + final CellControllerBuilder cellControllerBuilder; + const ChecklistCardCell({required this.cellControllerBuilder, Key? key}) + : super(key: key); + + @override + State createState() => _ChecklistCardCellState(); +} + +class _ChecklistCardCellState extends State { + late ChecklistCardCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as ChecklistCellController; + _cellBloc = ChecklistCardCellBloc(cellController: cellController); + _cellBloc.add(const ChecklistCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ChecklistProgressBar(percent: state.percent), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart new file mode 100644 index 0000000000..ca16ffe303 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../bloc/date_card_cell_bloc.dart'; +import '../define.dart'; +import 'card_cell.dart'; + +class DateCardCell extends CardCell { + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook? renderHook; + + const DateCardCell({ + required this.cellControllerBuilder, + this.renderHook, + Key? key, + }) : super(key: key); + + @override + State createState() => _DateCardCellState(); +} + +class _DateCardCellState extends State { + late DateCardCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as DateCellController; + + _cellBloc = DateCardCellBloc(cellController: cellController) + ..add(const DateCardCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.dateStr != current.dateStr, + builder: (context, state) { + if (state.dateStr.isEmpty) { + return const SizedBox(); + } else { + final Widget? custom = widget.renderHook?.call( + state.data, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: CardSizes.cardCellVPadding, + ), + child: FlowyText.regular( + state.dateStr, + fontSize: 13, + color: Theme.of(context).hintColor, + ), + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/number_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/number_card_cell.dart new file mode 100644 index 0000000000..c2366a5d8e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/number_card_cell.dart @@ -0,0 +1,88 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../bloc/number_card_cell_bloc.dart'; +import '../define.dart'; +import 'card_cell.dart'; + +class NumberCardCellStyle extends CardCellStyle { + final double fontSize; + + NumberCardCellStyle(this.fontSize); +} + +class NumberCardCell + extends CardCell { + final CellRenderHook? renderHook; + final CellControllerBuilder cellControllerBuilder; + + const NumberCardCell({ + required this.cellControllerBuilder, + CustomCardData? cardData, + NumberCardCellStyle? style, + this.renderHook, + Key? key, + }) : super(key: key, style: style, cardData: cardData); + + @override + State createState() => _NumberCardCellState(); +} + +class _NumberCardCellState extends State { + late NumberCardCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as NumberCellController; + + _cellBloc = NumberCardCellBloc(cellController: cellController) + ..add(const NumberCardCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox(); + } else { + final Widget? custom = widget.renderHook?.call( + state.content, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: CardSizes.cardCellVPadding, + ), + child: FlowyText.medium( + state.content, + fontSize: widget.style?.fontSize ?? 14, + ), + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart new file mode 100644 index 0000000000..13409e8581 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart @@ -0,0 +1,118 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.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 '../bloc/select_option_card_cell_bloc.dart'; +import 'card_cell.dart'; + +class SelectOptionCardCellStyle extends CardCellStyle {} + +class SelectOptionCardCell + extends CardCell + with EditableCell { + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook, CustomCardData>? renderHook; + + @override + final EditableCardNotifier? editableNotifier; + + SelectOptionCardCell({ + required this.cellControllerBuilder, + required CustomCardData? cardData, + this.renderHook, + this.editableNotifier, + Key? key, + }) : super(key: key, cardData: cardData); + + @override + State createState() => _SelectOptionCardCellState(); +} + +class _SelectOptionCardCellState extends State { + late SelectOptionCardCellBloc _cellBloc; + late PopoverController _popover; + + @override + void initState() { + _popover = PopoverController(); + final cellController = + widget.cellControllerBuilder.build() as SelectOptionCellController; + _cellBloc = SelectOptionCardCellBloc(cellController: cellController) + ..add(const SelectOptionCardCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) { + return previous.selectedOptions != current.selectedOptions; + }, + builder: (context, state) { + final Widget? custom = widget.renderHook?.call( + state.selectedOptions, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + final children = state.selectedOptions.map( + (option) { + final tag = SelectOptionTag.fromOption( + context: context, + option: option, + onSelected: () => _popover.show(), + ); + return _wrapPopover(tag); + }, + ).toList(); + + return IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: SizedBox.expand( + child: Wrap(spacing: 4, runSpacing: 2, children: children), + ), + ), + ); + }, + ), + ); + } + + Widget _wrapPopover(Widget child) { + final constraints = BoxConstraints.loose( + Size( + SelectOptionCellEditor.editorPanelWidth, + 300, + ), + ); + return AppFlowyPopover( + controller: _popover, + constraints: constraints, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext context) { + return SelectOptionCellEditor( + cellController: widget.cellControllerBuilder.build() + as SelectOptionCellController, + ); + }, + onClose: () {}, + child: child, + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart new file mode 100644 index 0000000000..25cb563f5c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart @@ -0,0 +1,195 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../row/cell_builder.dart'; +import '../bloc/text_card_cell_bloc.dart'; +import '../define.dart'; +import 'card_cell.dart'; + +class TextCardCellStyle extends CardCellStyle { + final double fontSize; + + TextCardCellStyle(this.fontSize); +} + +class TextCardCell + extends CardCell with EditableCell { + @override + final EditableCardNotifier? editableNotifier; + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook? renderHook; + + const TextCardCell({ + required this.cellControllerBuilder, + required CustomCardData? cardData, + this.editableNotifier, + this.renderHook, + TextCardCellStyle? style, + Key? key, + }) : super(key: key, style: style, cardData: cardData); + + @override + State createState() => _TextCardCellState(); +} + +class _TextCardCellState extends State { + late TextCardCellBloc _cellBloc; + late TextEditingController _controller; + bool focusWhenInit = false; + final focusNode = SingleListenerFocusNode(); + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as TextCellController; + _cellBloc = TextCardCellBloc(cellController: cellController) + ..add(const TextCardCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); + focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false; + if (focusWhenInit) { + focusNode.requestFocus(); + } + + // 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(() { + if (!focusNode.hasFocus) { + focusWhenInit = false; + widget.editableNotifier?.isCellEditing.value = false; + _cellBloc.add(const TextCardCellEvent.enableEdit(false)); + } + }); + _bindEditableNotifier(); + super.initState(); + } + + void _bindEditableNotifier() { + widget.editableNotifier?.isCellEditing.addListener(() { + if (!mounted) return; + + final isEditing = widget.editableNotifier?.isCellEditing.value ?? false; + if (isEditing) { + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + } + _cellBloc.add(TextCardCellEvent.enableEdit(isEditing)); + }); + } + + @override + void didUpdateWidget(covariant TextCardCell oldWidget) { + _bindEditableNotifier(); + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocListener( + listener: (context, state) { + if (_controller.text != state.content) { + _controller.text = state.content; + } + }, + child: BlocBuilder( + buildWhen: (previous, current) { + if (previous.content != current.content && + _controller.text == current.content && + current.enableEdit) { + return false; + } + + return previous != current; + }, + builder: (context, state) { + // Returns a custom render widget + final Widget? custom = widget.renderHook?.call( + state.content, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + if (state.content.isEmpty && + state.enableEdit == false && + focusWhenInit == false) { + return const SizedBox(); + } + + // + Widget child; + if (state.enableEdit || focusWhenInit) { + child = _buildTextField(); + } else { + child = _buildText(state); + } + return Align(alignment: Alignment.centerLeft, child: child); + }, + ), + ), + ); + } + + Future focusChanged() async { + _cellBloc.add(TextCardCellEvent.updateText(_controller.text)); + } + + @override + Future dispose() async { + _cellBloc.close(); + _controller.dispose(); + focusNode.dispose(); + super.dispose(); + } + + double _fontSize() { + if (widget.style != null) { + return widget.style!.fontSize; + } else { + return 14; + } + } + + Widget _buildText(TextCardCellState state) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: CardSizes.cardCellVPadding, + ), + child: FlowyText.medium( + state.content, + fontSize: _fontSize(), + maxLines: null, // Enable multiple lines + ), + ); + } + + Widget _buildTextField() { + return IntrinsicHeight( + child: TextField( + controller: _controller, + focusNode: focusNode, + onChanged: (value) => focusChanged(), + onEditingComplete: () => focusNode.unfocus(), + maxLines: null, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(fontSize: _fontSize()), + decoration: InputDecoration( + // Magic number 4 makes the textField take up the same space as FlowyText + contentPadding: EdgeInsets.symmetric( + vertical: CardSizes.cardCellVPadding + 4, + ), + border: InputBorder.none, + isDense: true, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/url_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/url_card_cell.dart new file mode 100644 index 0000000000..70b7034e22 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/url_card_cell.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../bloc/url_card_cell_bloc.dart'; +import '../define.dart'; +import 'card_cell.dart'; + +class URLCardCellStyle extends CardCellStyle { + final double fontSize; + + URLCardCellStyle(this.fontSize); +} + +class URLCardCell + extends CardCell { + final CellControllerBuilder cellControllerBuilder; + + const URLCardCell({ + required this.cellControllerBuilder, + URLCardCellStyle? style, + Key? key, + }) : super(key: key, style: style); + + @override + State createState() => _URLCardCellState(); +} + +class _URLCardCellState extends State { + late URLCardCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as URLCellController; + _cellBloc = URLCardCellBloc(cellController: cellController); + _cellBloc.add(const URLCardCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: CardSizes.cardCellVPadding, + ), + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: widget.style?.fontSize ?? FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ), + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/accessory.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/accessory.dart new file mode 100644 index 0000000000..4411634a69 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/accessory.dart @@ -0,0 +1,72 @@ +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +enum AccessoryType { + edit, + more, +} + +abstract mixin class CardAccessory implements Widget { + AccessoryType get type; + void onTap(BuildContext context) {} +} + +typedef CardAccessoryBuilder = List Function( + BuildContext buildContext, +); + +class CardAccessoryContainer extends StatelessWidget { + final void Function(AccessoryType) onTapAccessory; + final List accessories; + const CardAccessoryContainer({ + required this.accessories, + required this.onTapAccessory, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final children = accessories.map((accessory) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + accessory.onTap(context); + onTapAccessory(accessory.type); + }, + child: _wrapHover(context, accessory), + ); + }).toList(); + return _wrapDecoration(context, Row(children: children)); + } + + FlowyHover _wrapHover(BuildContext context, CardAccessory accessory) { + return FlowyHover( + style: HoverStyle( + backgroundColor: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.zero, + ), + builder: (_, onHover) => SizedBox( + width: 24, + height: 24, + child: accessory, + ), + ); + } + + Widget _wrapDecoration(BuildContext context, Widget child) { + final borderSide = BorderSide( + color: Theme.of(context).dividerColor, + width: 1.0, + ); + final decoration = BoxDecoration( + color: Colors.transparent, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ); + return Container( + clipBehavior: Clip.hardEdge, + decoration: decoration, + child: child, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart new file mode 100644 index 0000000000..5de39625e9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'accessory.dart'; + +class RowCardContainer extends StatelessWidget { + final Widget child; + final CardAccessoryBuilder? accessoryBuilder; + final bool Function()? buildAccessoryWhen; + final void Function(BuildContext) openCard; + final void Function(AccessoryType) openAccessory; + const RowCardContainer({ + required this.child, + required this.openCard, + required this.openAccessory, + this.accessoryBuilder, + this.buildAccessoryWhen, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => _CardContainerNotifier(), + child: Consumer<_CardContainerNotifier>( + builder: (context, notifier, _) { + Widget container = Center(child: child); + bool shouldBuildAccessory = true; + if (buildAccessoryWhen != null) { + shouldBuildAccessory = buildAccessoryWhen!.call(); + } + + if (accessoryBuilder != null && shouldBuildAccessory) { + final accessories = accessoryBuilder!(context); + if (accessories.isNotEmpty) { + container = _CardEnterRegion( + accessories: accessories, + onTapAccessory: openAccessory, + child: container, + ); + } + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => openCard(context), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: container, + ), + ); + }, + ), + ); + } +} + +class _CardEnterRegion extends StatelessWidget { + final Widget child; + final List accessories; + final void Function(AccessoryType) onTapAccessory; + const _CardEnterRegion({ + required this.child, + required this.accessories, + required this.onTapAccessory, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector<_CardContainerNotifier, bool>( + selector: (context, notifier) => notifier.onEnter, + builder: (context, onEnter, _) { + final List children = [child]; + if (onEnter) { + children.add( + Positioned( + top: 8.0, + right: 8.0, + child: CardAccessoryContainer( + accessories: accessories, + onTapAccessory: onTapAccessory, + ), + ), + ); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = false, + child: IntrinsicHeight( + child: Stack( + alignment: AlignmentDirectional.topEnd, + fit: StackFit.expand, + children: children, + ), + ), + ); + }, + ); + } +} + +class _CardContainerNotifier extends ChangeNotifier { + bool _onEnter = false; + + _CardContainerNotifier(); + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/define.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/define.dart new file mode 100644 index 0000000000..f07f867e7e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/define.dart @@ -0,0 +1,3 @@ +class CardSizes { + static double get cardCellVPadding => 6; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart new file mode 100644 index 0000000000..43b0faa6bf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart @@ -0,0 +1,31 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; + +extension DatabaseLayoutExtension on DatabaseLayoutPB { + String layoutName() { + switch (this) { + case DatabaseLayoutPB.Board: + return LocaleKeys.board_menuName.tr(); + case DatabaseLayoutPB.Calendar: + return LocaleKeys.calendar_menuName.tr(); + case DatabaseLayoutPB.Grid: + return LocaleKeys.grid_menuName.tr(); + default: + return ""; + } + } + + String iconName() { + switch (this) { + case DatabaseLayoutPB.Board: + return 'editor/board'; + case DatabaseLayoutPB.Calendar: + return "editor/grid"; + case DatabaseLayoutPB.Grid: + return "editor/grid"; + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart new file mode 100644 index 0000000000..86e7390ec3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; + +class DatabaseViewWidget extends StatefulWidget { + const DatabaseViewWidget({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + State createState() => _DatabaseViewWidgetState(); +} + +class _DatabaseViewWidgetState extends State { + /// Listens to the view updates. + late final ViewListener _listener; + + /// Notifies the view layout type changes. When the layout type changes, + /// the widget of the view will be updated. + late final ValueNotifier _layoutTypeChangeNotifier; + + /// The view will be updated by the [ViewListener]. + late ViewPB view; + + @override + void initState() { + super.initState(); + + view = widget.view; + _listenOnViewUpdated(); + } + + @override + void dispose() { + _layoutTypeChangeNotifier.dispose(); + _listener.stop(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _layoutTypeChangeNotifier, + builder: (_, __, ___) { + return view.plugin().widgetBuilder.buildWidget(); + }, + ); + } + + void _listenOnViewUpdated() { + _listener = ViewListener(viewId: widget.view.id) + ..start( + onViewUpdated: (updatedView) { + if (mounted) { + view = updatedView; + _layoutTypeChangeNotifier.value = view.layout; + } + }, + ); + + _layoutTypeChangeNotifier = ValueNotifier(widget.view.layout); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart new file mode 100644 index 0000000000..b290691693 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart @@ -0,0 +1,154 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.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 '../../grid/presentation/layout/sizes.dart'; +import '../../grid/presentation/widgets/header/field_editor.dart'; + +class DatabasePropertyList extends StatefulWidget { + final String viewId; + final FieldController fieldController; + const DatabasePropertyList({ + required this.viewId, + required this.fieldController, + Key? key, + }) : super(key: key); + + @override + State createState() => _DatabasePropertyListState(); +} + +class _DatabasePropertyListState extends State { + late PopoverMutex _popoverMutex; + + @override + void initState() { + _popoverMutex = PopoverMutex(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt( + param1: widget.viewId, + param2: widget.fieldController, + )..add(const DatabasePropertyEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final cells = state.fieldContexts.map((field) { + return _GridPropertyCell( + popoverMutex: _popoverMutex, + viewId: widget.viewId, + fieldInfo: field, + key: ValueKey(field.id), + ); + }).toList(); + + return ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) => cells[index], + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + padding: const EdgeInsets.symmetric(vertical: 6.0), + ); + }, + ), + ); + } +} + +class _GridPropertyCell extends StatefulWidget { + final FieldInfo fieldInfo; + final String viewId; + final PopoverMutex popoverMutex; + + const _GridPropertyCell({ + required this.viewId, + required this.fieldInfo, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + State<_GridPropertyCell> createState() => _GridPropertyCellState(); +} + +class _GridPropertyCellState extends State<_GridPropertyCell> { + late PopoverController _popoverController; + + @override + void initState() { + _popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final checkmark = svgWidget( + widget.fieldInfo.visibility ? 'home/show' : 'home/hide', + color: Theme.of(context).iconTheme.color, + ); + + return SizedBox( + height: GridSize.popoverItemHeight, + child: _editFieldButton(context, checkmark), + ); + } + + Widget _editFieldButton(BuildContext context, Widget checkmark) { + return AppFlowyPopover( + mutex: widget.popoverMutex, + controller: _popoverController, + offset: const Offset(8, 0), + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(240, 400)), + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + widget.fieldInfo.name, + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: svgWidget( + widget.fieldInfo.fieldType.iconName(), + color: Theme.of(context).iconTheme.color, + ), + rightIcon: FlowyIconButton( + hoverColor: Colors.transparent, + onPressed: () { + context.read().add( + DatabasePropertyEvent.setFieldVisibility( + widget.fieldInfo.id, + !widget.fieldInfo.visibility, + ), + ); + }, + icon: checkmark.padding(all: 6.0), + ), + onTap: () => _popoverController.show(), + ).padding(horizontal: 6.0), + popupBuilder: (BuildContext context) { + return FieldEditor( + viewId: widget.viewId, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.viewId, + field: widget.fieldInfo.field, + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart new file mode 100644 index 0000000000..a85616e51c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart @@ -0,0 +1,105 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:flowy_infra/image.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'; + +class DatabaseGroupList extends StatelessWidget { + final String viewId; + final FieldController fieldController; + final VoidCallback onDismissed; + const DatabaseGroupList({ + required this.viewId, + required this.fieldController, + required this.onDismissed, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DatabaseGroupBloc( + viewId: viewId, + fieldController: fieldController, + )..add(const DatabaseGroupEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final cells = state.fieldContexts.map((fieldInfo) { + Widget cell = _GridGroupCell( + fieldInfo: fieldInfo, + onSelected: () => onDismissed(), + key: ValueKey(fieldInfo.id), + ); + + if (!fieldInfo.canBeGroup) { + cell = IgnorePointer(child: Opacity(opacity: 0.3, child: cell)); + } + return cell; + }).toList(); + + return ListView.separated( + shrinkWrap: true, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) => cells[index], + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + padding: const EdgeInsets.all(6.0), + ); + }, + ), + ); + } +} + +class _GridGroupCell extends StatelessWidget { + final VoidCallback onSelected; + final FieldInfo fieldInfo; + const _GridGroupCell({ + required this.fieldInfo, + required this.onSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget? rightIcon; + if (fieldInfo.isGroupField) { + rightIcon = Padding( + padding: const EdgeInsets.all(2.0), + child: svgWidget("grid/checkmark"), + ); + } + + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + fieldInfo.name, + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: svgWidget( + fieldInfo.fieldType.iconName(), + color: Theme.of(context).iconTheme.color, + ), + rightIcon: rightIcon, + onTap: () { + context.read().add( + DatabaseGroupEvent.setGroupByField( + fieldInfo.id, + fieldInfo.fieldType, + ), + ); + onSelected(); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart new file mode 100644 index 0000000000..b1faeae3df --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart @@ -0,0 +1,204 @@ +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +import '../cell_builder.dart'; + +class GridCellAccessoryBuildContext { + final BuildContext anchorContext; + final bool isCellEditing; + + GridCellAccessoryBuildContext({ + required this.anchorContext, + required this.isCellEditing, + }); +} + +class GridCellAccessoryBuilder> { + final GlobalKey _key = GlobalKey(); + + final Widget Function(Key key) _builder; + + GridCellAccessoryBuilder({required Widget Function(Key key) builder}) + : _builder = builder; + + Widget build() => _builder(_key); + + void onTap() { + (_key.currentState as GridCellAccessoryState).onTap(); + } + + bool enable() { + if (_key.currentState == null) { + return true; + } + return (_key.currentState as GridCellAccessoryState).enable(); + } +} + +abstract mixin class GridCellAccessoryState { + void onTap(); + + // The accessory will be hidden if enable() return false; + bool enable() => true; +} + +class PrimaryCellAccessory extends StatefulWidget { + final VoidCallback onTapCallback; + final bool isCellEditing; + const PrimaryCellAccessory({ + required this.onTapCallback, + required this.isCellEditing, + Key? key, + }) : super(key: key); + + @override + State createState() => _PrimaryCellAccessoryState(); +} + +class _PrimaryCellAccessoryState extends State + with GridCellAccessoryState { + @override + Widget build(BuildContext context) { + return Tooltip( + message: LocaleKeys.tooltip_openAsPage.tr(), + child: SizedBox( + width: 26, + height: 26, + child: Padding( + padding: const EdgeInsets.all(3.0), + child: svgWidget( + "grid/expander", + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + } + + @override + void onTap() => widget.onTapCallback(); + + @override + bool enable() => !widget.isCellEditing; +} + +class AccessoryHover extends StatefulWidget { + final CellAccessory child; + final EdgeInsets contentPadding; + const AccessoryHover({ + required this.child, + this.contentPadding = EdgeInsets.zero, + Key? key, + }) : super(key: key); + + @override + State createState() => _AccessoryHoverState(); +} + +class _AccessoryHoverState extends State { + late AccessoryHoverState _hoverState; + VoidCallback? _listenerFn; + + @override + void initState() { + _hoverState = AccessoryHoverState(); + _listenerFn = () => + _hoverState.onHover = widget.child.onAccessoryHover?.value ?? false; + widget.child.onAccessoryHover?.addListener(_listenerFn!); + + super.initState(); + } + + @override + void dispose() { + _hoverState.dispose(); + + if (_listenerFn != null) { + widget.child.onAccessoryHover?.removeListener(_listenerFn!); + _listenerFn = null; + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final List children = [ + Padding(padding: widget.contentPadding, child: widget.child), + ]; + + final accessoryBuilder = widget.child.accessoryBuilder; + if (accessoryBuilder != null) { + final accessories = accessoryBuilder( + (GridCellAccessoryBuildContext( + anchorContext: context, + isCellEditing: false, + )), + ); + children.add( + Padding( + padding: const EdgeInsets.only(right: 6), + child: CellAccessoryContainer(accessories: accessories), + ).positioned(right: 0), + ); + } + + return ChangeNotifierProvider.value( + value: _hoverState, + child: MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => setState(() => _hoverState.onHover = true), + onExit: (p) => setState(() => _hoverState.onHover = false), + child: Stack( + fit: StackFit.loose, + alignment: AlignmentDirectional.center, + children: children, + ), + ), + ); + } +} + +class AccessoryHoverState extends ChangeNotifier { + bool _onHover = false; + + set onHover(bool value) { + if (_onHover != value) { + _onHover = value; + notifyListeners(); + } + } + + bool get onHover => _onHover; +} + +class CellAccessoryContainer extends StatelessWidget { + final List accessories; + const CellAccessoryContainer({required this.accessories, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final children = + accessories.where((accessory) => accessory.enable()).map((accessory) { + final hover = FlowyHover( + style: + HoverStyle(hoverColor: AFThemeExtension.of(context).lightGreyHover), + builder: (_, onHover) => accessory.build(), + ); + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => accessory.onTap(), + child: hover, + ); + }).toList(); + + return Wrap(spacing: 6, children: children); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_decoration.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_decoration.dart new file mode 100755 index 0000000000..0e74073e14 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_decoration.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class CellDecoration { + static BoxDecoration box({required Color color}) { + return BoxDecoration( + border: Border.all(color: Colors.black26, width: 0.2), + color: color, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_shortcuts.dart new file mode 100644 index 0000000000..f44a4b5663 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_shortcuts.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +typedef CellKeyboardAction = dynamic Function(); + +enum CellKeyboardKey { + onEnter, + onCopy, + onInsert, +} + +abstract class CellShortcuts extends Widget { + const CellShortcuts({Key? key}) : super(key: key); + + Map get shortcutHandlers; +} + +class GridCellShortcuts extends StatelessWidget { + final CellShortcuts child; + const GridCellShortcuts({required this.child, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(), + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): + const GridCellCopyIntent(), + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): + const GridCellPasteIntent(), + }, + child: Actions( + actions: { + GridCellEnterIdent: GridCellEnterAction(child: child), + GridCellCopyIntent: GridCellCopyAction(child: child), + GridCellPasteIntent: GridCellPasteAction(child: child), + }, + child: child, + ), + ); + } +} + +class GridCellEnterIdent extends Intent { + const GridCellEnterIdent(); +} + +class GridCellEnterAction extends Action { + final CellShortcuts child; + GridCellEnterAction({required this.child}); + + @override + void invoke(covariant GridCellEnterIdent intent) { + final callback = child.shortcutHandlers[CellKeyboardKey.onEnter]; + if (callback != null) { + callback(); + } + } +} + +class GridCellCopyIntent extends Intent { + const GridCellCopyIntent(); +} + +class GridCellCopyAction extends Action { + final CellShortcuts child; + GridCellCopyAction({required this.child}); + + @override + void invoke(covariant GridCellCopyIntent intent) { + final callback = child.shortcutHandlers[CellKeyboardKey.onCopy]; + if (callback == null) { + return; + } + + final s = callback(); + if (s is String) { + Clipboard.setData(ClipboardData(text: s)); + } + } +} + +class GridCellPasteIntent extends Intent { + const GridCellPasteIntent(); +} + +class GridCellPasteAction extends Action { + final CellShortcuts child; + GridCellPasteAction({required this.child}); + + @override + void invoke(covariant GridCellPasteIntent intent) { + final callback = child.shortcutHandlers[CellKeyboardKey.onInsert]; + if (callback != null) { + callback(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart new file mode 100755 index 0000000000..4731c72d2f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -0,0 +1,281 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import '../../application/cell/cell_service.dart'; +import 'accessory/cell_accessory.dart'; +import 'accessory/cell_shortcuts.dart'; +import 'cells/checkbox_cell/checkbox_cell.dart'; +import 'cells/checklist_cell/checklist_cell.dart'; +import 'cells/date_cell/date_cell.dart'; +import 'cells/number_cell/number_cell.dart'; +import 'cells/select_option_cell/select_option_cell.dart'; +import 'cells/text_cell/text_cell.dart'; +import 'cells/url_cell/url_cell.dart'; + +/// Build the cell widget in Grid style. +class GridCellBuilder { + final CellCache cellCache; + GridCellBuilder({ + required this.cellCache, + }); + + GridCellWidget build( + DatabaseCellContext cellContext, { + GridCellStyle? style, + }) { + final cellControllerBuilder = CellControllerBuilder( + cellContext: cellContext, + cellCache: cellCache, + ); + + final key = cellContext.key(); + switch (cellContext.fieldType) { + case FieldType.Checkbox: + return GridCheckboxCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.DateTime: + return GridDateCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + style: style, + fieldType: cellContext.fieldType, + ); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return GridDateCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + editable: false, + style: style, + fieldType: cellContext.fieldType, + ); + case FieldType.SingleSelect: + return GridSingleSelectCell( + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); + case FieldType.MultiSelect: + return GridMultiSelectCell( + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); + case FieldType.Checklist: + return GridChecklistCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.Number: + return GridNumberCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.RichText: + return GridTextCell( + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); + case FieldType.URL: + return GridURLCell( + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); + } + + throw UnimplementedError; + } +} + +class BlankCell extends StatelessWidget { + const BlankCell({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} + +abstract class CellEditable { + GridCellFocusListener get beginFocus; + + ValueNotifier get onCellFocus; + + ValueNotifier get onCellEditing; +} + +typedef AccessoryBuilder = List Function( + GridCellAccessoryBuildContext buildContext, +); + +abstract class CellAccessory extends Widget { + const CellAccessory({Key? key}) : super(key: key); + + // The hover will show if the isHover's value is true + ValueNotifier? get onAccessoryHover; + + AccessoryBuilder? get accessoryBuilder; +} + +abstract class GridCellWidget extends StatefulWidget + implements CellAccessory, CellEditable, CellShortcuts { + GridCellWidget({Key? key}) : super(key: key) { + onCellEditing.addListener(() { + onCellFocus.value = onCellEditing.value; + }); + } + + @override + final ValueNotifier onCellFocus = ValueNotifier(false); + + // When the cell is focused, we assume that the accessory also be hovered. + @override + ValueNotifier get onAccessoryHover => onCellFocus; + + @override + final ValueNotifier onCellEditing = ValueNotifier(false); + + @override + List Function( + GridCellAccessoryBuildContext buildContext, + )? get accessoryBuilder => null; + + @override + final GridCellFocusListener beginFocus = GridCellFocusListener(); + + @override + final Map shortcutHandlers = {}; +} + +abstract class GridCellState extends State { + @override + void initState() { + widget.beginFocus.setListener(() => requestBeginFocus()); + widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy(); + widget.shortcutHandlers[CellKeyboardKey.onInsert] = () { + Clipboard.getData("text/plain").then((data) { + final s = data?.text; + if (s is String) { + onInsert(s); + } + }); + }; + super.initState(); + } + + @override + void didUpdateWidget(covariant T oldWidget) { + if (oldWidget != this) { + widget.beginFocus.setListener(() => requestBeginFocus()); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + widget.beginFocus.removeAllListener(); + super.dispose(); + } + + void requestBeginFocus(); + + String? onCopy() => null; + + void onInsert(String value) {} +} + +abstract class GridFocusNodeCellState + extends GridCellState { + SingleListenerFocusNode focusNode = SingleListenerFocusNode(); + + @override + void initState() { + widget.shortcutHandlers[CellKeyboardKey.onEnter] = + () => focusNode.unfocus(); + _listenOnFocusNodeChanged(); + super.initState(); + } + + @override + void didUpdateWidget(covariant T oldWidget) { + if (oldWidget != this) { + _listenOnFocusNodeChanged(); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + widget.shortcutHandlers.clear(); + focusNode.removeAllListener(); + focusNode.dispose(); + super.dispose(); + } + + @override + void requestBeginFocus() { + if (focusNode.hasFocus == false && focusNode.canRequestFocus) { + FocusScope.of(context).requestFocus(focusNode); + } + } + + void _listenOnFocusNodeChanged() { + widget.onCellEditing.value = focusNode.hasFocus; + focusNode.setListener(() { + widget.onCellEditing.value = focusNode.hasFocus; + focusChanged(); + }); + } + + Future focusChanged() async {} +} + +class GridCellFocusListener extends ChangeNotifier { + VoidCallback? _listener; + + void setListener(VoidCallback listener) { + if (_listener != null) { + removeListener(_listener!); + } + + _listener = listener; + addListener(listener); + } + + void removeAllListener() { + if (_listener != null) { + removeListener(_listener!); + } + } + + void notify() { + notifyListeners(); + } +} + +abstract class GridCellStyle {} + +class SingleListenerFocusNode extends FocusNode { + VoidCallback? _listener; + + void setListener(VoidCallback listener) { + if (_listener != null) { + removeListener(_listener!); + } + + _listener = listener; + super.addListener(listener); + } + + void removeAllListener() { + if (_listener != null) { + removeListener(_listener!); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart new file mode 100644 index 0000000000..204a99687b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cell_container.dart @@ -0,0 +1,165 @@ +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 '../accessory/cell_accessory.dart'; +import '../accessory/cell_shortcuts.dart'; +import '../cell_builder.dart'; + +class CellContainer extends StatelessWidget { + final GridCellWidget child; + final AccessoryBuilder? accessoryBuilder; + final double width; + final bool isPrimary; + final CellContainerNotifier cellContainerNotifier; + + const CellContainer({ + Key? key, + required this.child, + required this.width, + required this.isPrimary, + required this.cellContainerNotifier, + this.accessoryBuilder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: cellContainerNotifier, + child: Selector( + selector: (context, notifier) => notifier.isFocus, + builder: (privderContext, isFocus, _) { + Widget container = Center(child: GridCellShortcuts(child: child)); + + if (accessoryBuilder != null) { + final accessories = accessoryBuilder!( + GridCellAccessoryBuildContext( + anchorContext: context, + isCellEditing: isFocus, + ), + ); + + if (accessories.isNotEmpty) { + container = _GridCellEnterRegion( + accessories: accessories, + isPrimary: isPrimary, + child: container, + ); + } + } + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => child.beginFocus.notify(), + child: Container( + constraints: BoxConstraints(maxWidth: width, minHeight: 46), + decoration: _makeBoxDecoration(context, isFocus), + child: container, + ), + ); + }, + ), + ); + } + + BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) { + if (isFocus) { + final borderSide = BorderSide( + color: Theme.of(context).colorScheme.primary, + ); + + return BoxDecoration(border: Border.fromBorderSide(borderSide)); + } + + final borderSide = BorderSide(color: Theme.of(context).dividerColor); + return BoxDecoration( + border: Border(right: borderSide, bottom: borderSide), + ); + } +} + +class _GridCellEnterRegion extends StatelessWidget { + final Widget child; + final List accessories; + final bool isPrimary; + const _GridCellEnterRegion({ + required this.child, + required this.accessories, + required this.isPrimary, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector2( + selector: (context, regionNotifier, cellNotifier) => + !cellNotifier.isFocus && + (cellNotifier.onEnter || regionNotifier.onEnter && isPrimary), + builder: (context, showAccessory, _) { + final List children = [child]; + if (showAccessory) { + children.add( + CellAccessoryContainer(accessories: accessories).positioned( + right: GridSize.cellContentInsets.right, + ), + ); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => + Provider.of(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of(context, listen: false) + .onEnter = false, + child: Stack( + alignment: AlignmentDirectional.center, + fit: StackFit.expand, + children: children, + ), + ); + }, + ); + } +} + +class CellContainerNotifier extends ChangeNotifier { + final CellEditable cellEditable; + VoidCallback? _onCellFocusListener; + bool _isFocus = false; + bool _onEnter = false; + + CellContainerNotifier(this.cellEditable) { + _onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value; + cellEditable.onCellFocus.addListener(_onCellFocusListener!); + } + + @override + void dispose() { + if (_onCellFocusListener != null) { + cellEditable.onCellFocus.removeListener(_onCellFocusListener!); + } + super.dispose(); + } + + set isFocus(bool value) { + if (_isFocus != value) { + _isFocus = value; + notifyListeners(); + } + } + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get isFocus => _isFocus; + + bool get onEnter => _onEnter; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart new file mode 100644 index 0000000000..0a76539284 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart @@ -0,0 +1,7 @@ +export 'checkbox_cell/checkbox_cell.dart'; +export 'checklist_cell/checklist_cell.dart'; +export 'date_cell/date_cell.dart'; +export 'number_cell/number_cell.dart'; +export 'select_option_cell/select_option_cell.dart'; +export 'text_cell/text_cell.dart'; +export 'url_cell/url_cell.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart new file mode 100644 index 0000000000..a8c5594545 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart @@ -0,0 +1,102 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'checkbox_cell_bloc.dart'; +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../cell_builder.dart'; + +class GridCheckboxCell extends GridCellWidget { + final CellControllerBuilder cellControllerBuilder; + GridCheckboxCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + GridCellState createState() => _CheckboxCellState(); +} + +class _CheckboxCellState extends GridCellState { + late CheckboxCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as CheckboxCellController; + _cellBloc = CheckboxCellBloc( + service: CellBackendService(), + cellController: cellController, + )..add(const CheckboxCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + final icon = state.isSelected + ? const CheckboxCellCheck() + : const CheckboxCellUncheck(); + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: GridSize.cellContentInsets, + child: FlowyIconButton( + hoverColor: Colors.transparent, + onPressed: () => context + .read() + .add(const CheckboxCellEvent.select()), + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + ), + ), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + void requestBeginFocus() { + _cellBloc.add(const CheckboxCellEvent.select()); + } + + @override + String? onCopy() { + if (_cellBloc.state.isSelected) { + return "Yes"; + } else { + return "No"; + } + } +} + +class CheckboxCellCheck extends StatelessWidget { + const CheckboxCellCheck({super.key}); + + @override + Widget build(BuildContext context) { + return svgWidget('editor/editor_check'); + } +} + +class CheckboxCellUncheck extends StatelessWidget { + const CheckboxCellUncheck({super.key}); + + @override + Widget build(BuildContext context) { + return svgWidget('editor/editor_uncheck'); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart new file mode 100644 index 0000000000..e0a2ac58db --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart @@ -0,0 +1,77 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'checkbox_cell_bloc.freezed.dart'; + +class CheckboxCellBloc extends Bloc { + final CheckboxCellController cellController; + void Function()? _onCellChangedFn; + + CheckboxCellBloc({ + required CellBackendService service, + required this.cellController, + }) : super(CheckboxCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () { + _startListening(); + }, + select: () async { + cellController.saveCellData(!state.isSelected ? "Yes" : "No"); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith(isSelected: _isSelected(cellData))); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(CheckboxCellEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class CheckboxCellEvent with _$CheckboxCellEvent { + const factory CheckboxCellEvent.initial() = _Initial; + const factory CheckboxCellEvent.select() = _Selected; + const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = + _DidReceiveCellUpdate; +} + +@freezed +class CheckboxCellState with _$CheckboxCellState { + const factory CheckboxCellState({ + required bool isSelected, + }) = _CheckboxCellState; + + factory CheckboxCellState.initial(TextCellController context) { + return CheckboxCellState(isSelected: _isSelected(context.getCellData())); + } +} + +bool _isSelected(String? cellData) { + return cellData == "Yes"; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart new file mode 100644 index 0000000000..47fa6d3df0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart @@ -0,0 +1,69 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.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 '../../cell_builder.dart'; +import 'checklist_cell_bloc.dart'; +import 'checklist_cell_editor.dart'; +import 'checklist_progress_bar.dart'; + +class GridChecklistCell extends GridCellWidget { + final CellControllerBuilder cellControllerBuilder; + GridChecklistCell({required this.cellControllerBuilder, Key? key}) + : super(key: key); + + @override + GridCellState createState() => GridChecklistCellState(); +} + +class GridChecklistCellState extends GridCellState { + late ChecklistCardCellBloc _cellBloc; + late final PopoverController _popover; + + @override + void initState() { + _popover = PopoverController(); + final cellController = + widget.cellControllerBuilder.build() as ChecklistCellController; + _cellBloc = ChecklistCardCellBloc(cellController: cellController); + _cellBloc.add(const ChecklistCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: AppFlowyPopover( + margin: EdgeInsets.zero, + controller: _popover, + constraints: BoxConstraints.loose(const Size(260, 400)), + direction: PopoverDirection.bottomWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + popupBuilder: (BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onCellEditing.value = true; + }); + return GridChecklistCellEditor( + cellController: + widget.cellControllerBuilder.build() as ChecklistCellController, + ); + }, + onClose: () => widget.onCellEditing.value = false, + child: Padding( + padding: GridSize.cellContentInsets, + child: BlocBuilder( + builder: (context, state) => + ChecklistProgressBar(percent: state.percent), + ), + ), + ), + ); + } + + @override + void requestBeginFocus() => _popover.show(); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart new file mode 100644 index 0000000000..d8445d7f4b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart @@ -0,0 +1,103 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/cell/checklist_cell_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +part 'checklist_cell_bloc.freezed.dart'; + +class ChecklistCardCellBloc + extends Bloc { + final ChecklistCellController cellController; + final ChecklistCellBackendService _checklistCellSvc; + void Function()? _onCellChangedFn; + ChecklistCardCellBloc({ + required this.cellController, + }) : _checklistCellSvc = ChecklistCellBackendService( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + rowId: cellController.rowId, + ), + super(ChecklistCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + _loadOptions(); + }, + didReceiveOptions: (data) { + emit( + state.copyWith( + allOptions: data.options, + selectedOptions: data.selectedOptions, + percent: data.percentage, + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellFieldChanged: () { + _loadOptions(); + }, + onCellChanged: (data) { + if (!isClosed && data != null) { + add(ChecklistCellEvent.didReceiveOptions(data)); + } + }, + ); + } + + void _loadOptions() { + _checklistCellSvc.getCellData().then((result) { + if (isClosed) return; + + return result.fold( + (data) => add(ChecklistCellEvent.didReceiveOptions(data)), + (err) => Log.error(err), + ); + }); + } +} + +@freezed +class ChecklistCellEvent with _$ChecklistCellEvent { + const factory ChecklistCellEvent.initial() = _InitialCell; + const factory ChecklistCellEvent.didReceiveOptions( + ChecklistCellDataPB data, + ) = _DidReceiveCellUpdate; +} + +@freezed +class ChecklistCellState with _$ChecklistCellState { + const factory ChecklistCellState({ + required List allOptions, + required List selectedOptions, + required double percent, + }) = _ChecklistCellState; + + factory ChecklistCellState.initial(ChecklistCellController cellController) { + return const ChecklistCellState( + allOptions: [], + selectedOptions: [], + percent: 0, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart new file mode 100644 index 0000000000..8ebcfe7b2a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart @@ -0,0 +1,185 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.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 '../../../../grid/presentation/layout/sizes.dart'; +import '../../../../grid/presentation/widgets/header/type_option/select_option_editor.dart'; +import 'checklist_cell_editor_bloc.dart'; +import 'checklist_progress_bar.dart'; + +class GridChecklistCellEditor extends StatefulWidget { + final ChecklistCellController cellController; + const GridChecklistCellEditor({required this.cellController, Key? key}) + : super(key: key); + + @override + State createState() => + _GridChecklistCellEditorState(); +} + +class _GridChecklistCellEditorState extends State { + late ChecklistCellEditorBloc bloc; + late PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + bloc = ChecklistCellEditorBloc(cellController: widget.cellController); + bloc.add(const ChecklistCellEditorEvent.initial()); + super.initState(); + } + + @override + void dispose() { + bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + final List slivers = [ + const SliverChecklistProgressBar(), + SliverToBoxAdapter( + child: ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: state.allOptions.length, + itemBuilder: (BuildContext context, int index) { + return _ChecklistOptionCell( + option: state.allOptions[index], + popoverMutex: popoverMutex, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + ), + ), + ]; + + return Padding( + padding: const EdgeInsets.all(8.0), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), + child: CustomScrollView( + shrinkWrap: true, + slivers: slivers, + controller: ScrollController(), + physics: StyledScrollPhysics(), + ), + ), + ); + }, + ), + ); + } +} + +class _ChecklistOptionCell extends StatefulWidget { + final ChecklistSelectOption option; + final PopoverMutex popoverMutex; + const _ChecklistOptionCell({ + required this.option, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + State<_ChecklistOptionCell> createState() => _ChecklistOptionCellState(); +} + +class _ChecklistOptionCellState extends State<_ChecklistOptionCell> { + late PopoverController _popoverController; + + @override + void initState() { + _popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final icon = widget.option.isSelected + ? svgWidget('editor/editor_check') + : svgWidget('editor/editor_uncheck'); + return _wrapPopover( + SizedBox( + height: GridSize.popoverItemHeight, + child: Row( + children: [ + Expanded( + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText( + widget.option.data.name, + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: icon, + onTap: () => context + .read() + .add(ChecklistCellEditorEvent.selectOption(widget.option)), + ), + ), + _disclosureButton(), + ], + ), + ), + ); + } + + Widget _disclosureButton() { + return FlowyIconButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + width: 20, + onPressed: () => _popoverController.show(), + iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), + icon: svgWidget( + "editor/details", + color: Theme.of(context).iconTheme.color, + ), + ); + } + + Widget _wrapPopover(Widget child) { + return AppFlowyPopover( + controller: _popoverController, + offset: const Offset(8, 0), + asBarrier: true, + constraints: BoxConstraints.loose(const Size(200, 300)), + mutex: widget.popoverMutex, + triggerActions: PopoverTriggerFlags.none, + child: child, + popupBuilder: (BuildContext popoverContext) { + return SelectOptionTypeOptionEditor( + option: widget.option.data, + onDeleted: () { + context.read().add( + ChecklistCellEditorEvent.deleteOption(widget.option.data), + ); + + _popoverController.close(); + }, + onUpdated: (updatedOption) { + context.read().add( + ChecklistCellEditorEvent.updateOption(updatedOption), + ); + }, + showOptions: false, + autoFocus: false, + // Use ValueKey to refresh the UI, otherwise, it will remain the old value. + key: ValueKey( + widget.option.data.id, + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart new file mode 100644 index 0000000000..f48608b389 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart @@ -0,0 +1,186 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/cell/checklist_cell_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'checklist_cell_editor_bloc.freezed.dart'; + +class ChecklistCellEditorBloc + extends Bloc { + final ChecklistCellBackendService _checklistCellService; + final ChecklistCellController cellController; + + ChecklistCellEditorBloc({ + required this.cellController, + }) : _checklistCellService = ChecklistCellBackendService( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + rowId: cellController.rowId, + ), + super(ChecklistCellEditorState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + _loadOptions(); + }, + didReceiveOptions: (data) { + emit( + state.copyWith( + allOptions: _makeChecklistSelectOptions(data, state.predicate), + percent: data.percentage, + ), + ); + }, + newOption: (optionName) { + _createOption(optionName); + emit( + state.copyWith( + createOption: Some(optionName), + predicate: '', + ), + ); + }, + deleteOption: (option) { + _deleteOption([option]); + }, + updateOption: (option) { + _updateOption(option); + }, + selectOption: (option) async { + await _checklistCellService.select(optionId: option.data.id); + }, + filterOption: (String predicate) {}, + ); + }, + ); + } + + @override + Future close() async { + await cellController.dispose(); + return super.close(); + } + + void _createOption(String name) async { + final result = await _checklistCellService.create(name: name); + result.fold((l) => {}, (err) => Log.error(err)); + } + + void _deleteOption(List options) async { + final result = await _checklistCellService.delete( + optionIds: options.map((e) => e.id).toList(), + ); + result.fold((l) => null, (err) => Log.error(err)); + } + + void _updateOption(SelectOptionPB option) async { + final result = await _checklistCellService.update( + option: option, + ); + + result.fold((l) => null, (err) => Log.error(err)); + } + + void _loadOptions() { + _checklistCellService.getCellData().then((result) { + if (isClosed) return; + + return result.fold( + (data) => add(ChecklistCellEditorEvent.didReceiveOptions(data)), + (err) => Log.error(err), + ); + }); + } + + void _startListening() { + cellController.startListening( + onCellChanged: ((data) { + if (!isClosed && data != null) { + add(ChecklistCellEditorEvent.didReceiveOptions(data)); + } + }), + onCellFieldChanged: () { + _loadOptions(); + }, + ); + } +} + +@freezed +class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent { + const factory ChecklistCellEditorEvent.initial() = _Initial; + const factory ChecklistCellEditorEvent.didReceiveOptions( + ChecklistCellDataPB data, + ) = _DidReceiveOptions; + const factory ChecklistCellEditorEvent.newOption(String optionName) = + _NewOption; + const factory ChecklistCellEditorEvent.selectOption( + ChecklistSelectOption option, + ) = _SelectOption; + const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) = + _UpdateOption; + const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) = + _DeleteOption; + const factory ChecklistCellEditorEvent.filterOption(String predicate) = + _FilterOption; +} + +@freezed +class ChecklistCellEditorState with _$ChecklistCellEditorState { + const factory ChecklistCellEditorState({ + required List allOptions, + required Option createOption, + required double percent, + required String predicate, + }) = _ChecklistCellEditorState; + + factory ChecklistCellEditorState.initial(ChecklistCellController context) { + final data = context.getCellData(loadIfNotExist: true); + + return ChecklistCellEditorState( + allOptions: _makeChecklistSelectOptions(data, ''), + createOption: none(), + percent: data?.percentage ?? 0, + predicate: '', + ); + } +} + +List _makeChecklistSelectOptions( + ChecklistCellDataPB? data, + String predicate, +) { + if (data == null) { + return []; + } + + final List options = []; + final List allOptions = List.from(data.options); + if (predicate.isNotEmpty) { + allOptions.retainWhere((element) => element.name.contains(predicate)); + } + final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList(); + + for (final option in allOptions) { + options.add( + ChecklistSelectOption(selectedOptionIds.contains(option.id), option), + ); + } + + return options; +} + +class ChecklistSelectOption { + final bool isSelected; + final SelectOptionPB data; + + ChecklistSelectOption(this.isSelected, this.data); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart new file mode 100644 index 0000000000..04fe36dcd0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart @@ -0,0 +1,129 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/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:percent_indicator/percent_indicator.dart'; + +class ChecklistProgressBar extends StatelessWidget { + final double percent; + const ChecklistProgressBar({required this.percent, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return LinearPercentIndicator( + lineHeight: 10.0, + percent: percent, + padding: EdgeInsets.zero, + progressColor: percent < 1.0 + ? SelectOptionColorPB.Purple.toColor(context) + : SelectOptionColorPB.Green.toColor(context), + backgroundColor: AFThemeExtension.of(context).progressBarBGColor, + barRadius: const Radius.circular(5), + ); + } +} + +class SliverChecklistProgressBar extends StatelessWidget { + const SliverChecklistProgressBar({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverPersistentHeader( + pinned: true, + delegate: _SliverChecklistProgressBarDelegate(), + ); + } +} + +class _SliverChecklistProgressBarDelegate + extends SliverPersistentHeaderDelegate { + _SliverChecklistProgressBarDelegate(); + + double fixHeight = 60; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return const _AutoFocusTextField(); + } + + @override + double get maxExtent => fixHeight; + + @override + double get minExtent => fixHeight; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return true; + } +} + +class _AutoFocusTextField extends StatefulWidget { + const _AutoFocusTextField(); + + @override + State<_AutoFocusTextField> createState() => _AutoFocusTextFieldState(); +} + +class _AutoFocusTextFieldState extends State<_AutoFocusTextField> { + final _focusNode = FocusNode(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return BlocListener( + listenWhen: (previous, current) => + previous.createOption != current.createOption, + listener: (context, state) { + if (_focusNode.canRequestFocus) { + _focusNode.requestFocus(); + } + }, + child: Container( + color: Theme.of(context).cardColor, + child: Padding( + padding: GridSize.typeOptionContentInsets, + child: Column( + children: [ + FlowyTextField( + autoFocus: true, + focusNode: _focusNode, + autoClearWhenDone: true, + submitOnLeave: true, + hintText: LocaleKeys.grid_checklist_panelTitle.tr(), + onChanged: (text) { + context + .read() + .add(ChecklistCellEditorEvent.filterOption(text)); + }, + onSubmitted: (text) { + context + .read() + .add(ChecklistCellEditorEvent.newOption(text)); + }, + ), + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: ChecklistProgressBar(percent: state.percent), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart new file mode 100644 index 0000000000..7c26d24498 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -0,0 +1,301 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart' + show StringTranslateExtension; +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:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'dart:async'; +import 'package:protobuf/protobuf.dart'; + +part 'date_cal_bloc.freezed.dart'; + +class DateCellCalendarBloc + extends Bloc { + final DateCellController cellController; + void Function()? _onCellChangedFn; + + DateCellCalendarBloc({ + required DateTypeOptionPB dateTypeOptionPB, + required DateCellDataPB? cellData, + required this.cellController, + }) : super(DateCellCalendarState.initial(dateTypeOptionPB, cellData)) { + on( + (event, emit) async { + await event.when( + initial: () async => _startListening(), + didReceiveCellUpdate: (DateCellDataPB? cellData) { + final dateData = _dateDataFromCellData(cellData); + emit( + state.copyWith( + dateTime: dateData.dateTime, + time: dateData.time, + includeTime: dateData.includeTime, + ), + ); + }, + didReceiveTimeFormatError: (String? timeFormatError) { + emit(state.copyWith(timeFormatError: timeFormatError)); + }, + selectDay: (date) async { + await _updateDateData(emit, date: date); + }, + setIncludeTime: (includeTime) async { + await _updateDateData(emit, includeTime: includeTime); + }, + setTime: (time) async { + await _updateDateData(emit, time: time); + }, + setDateFormat: (dateFormat) async { + await _updateTypeOption(emit, dateFormat: dateFormat); + }, + setTimeFormat: (timeFormat) async { + await _updateTypeOption(emit, timeFormat: timeFormat); + }, + setCalFormat: (format) { + emit(state.copyWith(format: format)); + }, + setFocusedDay: (focusedDay) { + emit(state.copyWith(focusedDay: focusedDay)); + }, + ); + }, + ); + } + + Future _updateDateData( + Emitter emit, { + DateTime? date, + String? time, + bool? includeTime, + }) async { + // make sure that not both date and time are updated at the same time + assert( + date == null && time == null || + date == null && time != null || + date != null && time == null, + ); + final String? newTime = time ?? state.time; + DateTime? newDate = _utcToLocalAddTime(date); + if (time != null && time.isNotEmpty) { + newDate = state.dateTime ?? DateTime.now(); + } + + final DateCellData newDateData = DateCellData( + dateTime: newDate, + time: newTime, + includeTime: includeTime ?? state.includeTime, + ); + + cellController.saveCellData( + newDateData, + onFinish: (result) { + result.fold( + () { + if (!isClosed && state.timeFormatError != null) { + add(const DateCellCalendarEvent.didReceiveTimeFormatError(null)); + } + }, + (err) { + switch (ErrorCode.valueOf(err.code)!) { + case ErrorCode.InvalidDateTimeFormat: + if (isClosed) return; + add( + DateCellCalendarEvent.didReceiveTimeFormatError( + timeFormatPrompt(err), + ), + ); + break; + default: + Log.error(err); + } + }, + ); + }, + ); + } + + DateTime? _utcToLocalAddTime(DateTime? date) { + if (date == null) { + return null; + } + final now = DateTime.now(); + // the incoming date is Utc. this trick converts it into Local + // and add the current time, though the time may be overwritten by + // explicitly provided time string + return DateTime( + date.year, + date.month, + date.day, + now.hour, + now.minute, + now.second, + ); + } + + String timeFormatPrompt(FlowyError error) { + String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}."; + switch (state.dateTypeOptionPB.timeFormat) { + case TimeFormatPB.TwelveHour: + msg = "$msg e.g. 01:00 PM"; + break; + case TimeFormatPB.TwentyFourHour: + msg = "$msg e.g. 13:00"; + break; + default: + break; + } + return msg; + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cell) { + if (!isClosed) { + add(DateCellCalendarEvent.didReceiveCellUpdate(cell)); + } + }), + ); + } + + Future? _updateTypeOption( + Emitter emit, { + DateFormatPB? dateFormat, + TimeFormatPB? timeFormat, + }) async { + state.dateTypeOptionPB.freeze(); + final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) { + if (dateFormat != null) { + typeOption.dateFormat = dateFormat; + } + + if (timeFormat != null) { + typeOption.timeFormat = timeFormat; + } + }); + + final result = await FieldBackendService.updateFieldTypeOption( + viewId: cellController.viewId, + fieldId: cellController.fieldInfo.id, + typeOptionData: newDateTypeOption.writeToBuffer(), + ); + + result.fold( + (l) => emit( + state.copyWith( + dateTypeOptionPB: newDateTypeOption, + timeHintText: _timeHintText(newDateTypeOption), + ), + ), + (err) => Log.error(err), + ); + } +} + +@freezed +class DateCellCalendarEvent with _$DateCellCalendarEvent { + // initial event + const factory DateCellCalendarEvent.initial() = _Initial; + + // notification that cell is updated in the backend + const factory DateCellCalendarEvent.didReceiveCellUpdate( + DateCellDataPB? data, + ) = _DidReceiveCellUpdate; + const factory DateCellCalendarEvent.didReceiveTimeFormatError( + String? timeformatError, + ) = _DidReceiveTimeFormatError; + + // table calendar's UI settings + const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay; + const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) = + _CalendarFormat; + + // date cell data is modified + const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay; + const factory DateCellCalendarEvent.setTime(String time) = _Time; + const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) = + _IncludeTime; + + // date field type options are modified + const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) = + _TimeFormat; + const factory DateCellCalendarEvent.setDateFormat(DateFormatPB dateFormat) = + _DateFormat; +} + +@freezed +class DateCellCalendarState with _$DateCellCalendarState { + const factory DateCellCalendarState({ + required DateTypeOptionPB dateTypeOptionPB, + required CalendarFormat format, + required DateTime focusedDay, + required DateTime? dateTime, + required String? time, + required bool includeTime, + required String? timeFormatError, + required String timeHintText, + }) = _DateCellCalendarState; + + factory DateCellCalendarState.initial( + DateTypeOptionPB dateTypeOptionPB, + DateCellDataPB? cellData, + ) { + final dateData = _dateDataFromCellData(cellData); + return DateCellCalendarState( + dateTypeOptionPB: dateTypeOptionPB, + format: CalendarFormat.month, + focusedDay: DateTime.now(), + dateTime: dateData.dateTime, + time: dateData.time, + includeTime: dateData.includeTime, + timeFormatError: null, + timeHintText: _timeHintText(dateTypeOptionPB), + ); + } +} + +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 ""; + } +} + +DateCellData _dateDataFromCellData(DateCellDataPB? cellData) { + // a null DateCellDataPB may be returned, indicating that all the fields are + // at their default values: empty strings and false booleans + if (cellData == null) { + return const DateCellData(includeTime: false); + } + + DateTime? dateTime; + String? time; + if (cellData.hasTimestamp()) { + final timestamp = cellData.timestamp * 1000; + dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); + time = cellData.time; + } + final bool includeTime = cellData.includeTime; + + return DateCellData(dateTime: dateTime, time: time, includeTime: includeTime); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart new file mode 100644 index 0000000000..7695529e26 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart @@ -0,0 +1,150 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; + +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../cell_builder.dart'; +import 'date_cell_bloc.dart'; +import 'date_editor.dart'; + +class DateCellStyle extends GridCellStyle { + Alignment alignment; + + DateCellStyle({this.alignment = Alignment.center}); +} + +abstract class GridCellDelegate { + void onFocus(bool isFocus); + GridCellDelegate get delegate; +} + +class GridDateCell extends GridCellWidget { + final bool editable; + + /// The [GridDateCell] is used by Field Type [FieldType.DateTime], + /// [FieldType.CreatedTime], [FieldType.LastEditedTime]. So it needs + /// to know the field type. + final FieldType fieldType; + final CellControllerBuilder cellControllerBuilder; + late final DateCellStyle? cellStyle; + + GridDateCell({ + GridCellStyle? style, + required this.fieldType, + required this.cellControllerBuilder, + this.editable = true, + Key? key, + }) : super(key: key) { + if (style != null) { + cellStyle = (style as DateCellStyle); + } else { + cellStyle = null; + } + } + + @override + GridCellState createState() => _DateCellState(); +} + +class _DateCellState extends GridCellState { + late PopoverController _popover; + late DateCellBloc _cellBloc; + + @override + void initState() { + _popover = PopoverController(); + final cellController = + widget.cellControllerBuilder.build() as DateCellController; + _cellBloc = DateCellBloc(cellController: cellController) + ..add(const DateCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final alignment = widget.cellStyle != null + ? widget.cellStyle!.alignment + : Alignment.centerLeft; + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + Widget dateTextWidget = GridDateCellText( + dateStr: state.dateStr, + alignment: alignment, + ); + + // If the cell is editable, wrap it in a popover. + if (widget.editable) { + dateTextWidget = AppFlowyPopover( + controller: _popover, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(260, 500)), + margin: EdgeInsets.zero, + child: dateTextWidget, + popupBuilder: (BuildContext popoverContent) { + return DateCellEditor( + cellController: widget.cellControllerBuilder.build() + as DateCellController, + onDismissed: () => widget.onCellEditing.value = false, + ); + }, + onClose: () { + widget.onCellEditing.value = false; + }, + ); + } + return dateTextWidget; + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + void requestBeginFocus() { + _popover.show(); + + if (widget.editable) { + widget.onCellEditing.value = true; + } + } + + @override + String? onCopy() => _cellBloc.state.dateStr; +} + +class GridDateCellText extends StatelessWidget { + final String dateStr; + final Alignment alignment; + const GridDateCellText({ + required this.dateStr, + required this.alignment, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Align( + alignment: alignment, + child: Padding( + padding: GridSize.cellContentInsets, + child: FlowyText.medium( + dateStr, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart new file mode 100644 index 0000000000..fd62119997 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart @@ -0,0 +1,89 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +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 'dart:async'; +part 'date_cell_bloc.freezed.dart'; + +class DateCellBloc extends Bloc { + final DateCellController cellController; + void Function()? _onCellChangedFn; + + DateCellBloc({required this.cellController}) + : super(DateCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () => _startListening(), + didReceiveCellUpdate: (DateCellDataPB? cellData) { + emit( + state.copyWith( + data: cellData, + dateStr: _dateStrFromCellData(cellData), + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((data) { + if (!isClosed) { + add(DateCellEvent.didReceiveCellUpdate(data)); + } + }), + ); + } +} + +@freezed +class DateCellEvent with _$DateCellEvent { + const factory DateCellEvent.initial() = _InitialCell; + const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = + _DidReceiveCellUpdate; +} + +@freezed +class DateCellState with _$DateCellState { + const factory DateCellState({ + required DateCellDataPB? data, + required String dateStr, + required FieldInfo fieldInfo, + }) = _DateCellState; + + factory DateCellState.initial(DateCellController context) { + final cellData = context.getCellData(); + + return DateCellState( + fieldInfo: context.fieldInfo, + data: cellData, + dateStr: _dateStrFromCellData(cellData), + ); + } +} + +String _dateStrFromCellData(DateCellDataPB? cellData) { + String dateStr = ""; + if (cellData != null) { + if (cellData.includeTime) { + dateStr = "${cellData.date} ${cellData.time}"; + } else { + dateStr = cellData.date; + } + } + return dateStr; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart new file mode 100644 index 0000000000..65e50bc8db --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -0,0 +1,459 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.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/date_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:dartz/dartz.dart' show Either; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/time/duration.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:table_calendar/table_calendar.dart'; +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../../../grid/presentation/widgets/common/type_option_separator.dart'; +import '../../../../grid/presentation/widgets/header/type_option/date.dart'; +import 'date_cal_bloc.dart'; + +final kFirstDay = DateTime.utc(1970, 1, 1); +final kLastDay = DateTime.utc(2100, 1, 1); + +class DateCellEditor extends StatefulWidget { + final VoidCallback onDismissed; + final DateCellController cellController; + + const DateCellEditor({ + Key? key, + required this.onDismissed, + required this.cellController, + }) : super(key: key); + + @override + State createState() => _DateCellEditor(); +} + +class _DateCellEditor extends State { + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: widget.cellController.getTypeOption( + DateTypeOptionDataParser(), + ), + builder: (BuildContext context, snapshot) { + if (snapshot.hasData) { + return _buildWidget(snapshot); + } else { + return const SizedBox(); + } + }, + ); + } + + Widget _buildWidget(AsyncSnapshot> snapshot) { + return snapshot.data!.fold( + (dateTypeOptionPB) { + return _CellCalendarWidget( + cellContext: widget.cellController, + dateTypeOptionPB: dateTypeOptionPB, + ); + }, + (err) { + Log.error(err); + return const SizedBox(); + }, + ); + } +} + +class _CellCalendarWidget extends StatefulWidget { + final DateCellController cellContext; + final DateTypeOptionPB dateTypeOptionPB; + + const _CellCalendarWidget({ + required this.cellContext, + required this.dateTypeOptionPB, + Key? key, + }) : super(key: key); + + @override + State<_CellCalendarWidget> createState() => _CellCalendarWidgetState(); +} + +class _CellCalendarWidgetState extends State<_CellCalendarWidget> { + late PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DateCellCalendarBloc( + dateTypeOptionPB: widget.dateTypeOptionPB, + cellData: widget.cellContext.getCellData(), + cellController: widget.cellContext, + )..add(const DateCellCalendarEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final List children = [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: _buildCalendar(context), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: state.includeTime + ? _TimeTextField( + timeStr: state.time, + popoverMutex: popoverMutex, + ) + : const SizedBox.shrink(), + ), + const TypeOptionSeparator(spacing: 12.0), + const _IncludeTimeButton(), + const TypeOptionSeparator(spacing: 12.0), + _DateTypeOptionButton(popoverMutex: popoverMutex) + ]; + + return ListView.builder( + shrinkWrap: true, + controller: ScrollController(), + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + padding: const EdgeInsets.symmetric(vertical: 12.0), + ); + }, + ), + ); + } + + @override + void dispose() { + popoverMutex.dispose(); + super.dispose(); + } + + Widget _buildCalendar(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final textStyle = Theme.of(context).textTheme.bodyMedium!; + final boxDecoration = BoxDecoration( + color: Theme.of(context).colorScheme.surface, + shape: BoxShape.rectangle, + borderRadius: Corners.s6Border, + ); + return TableCalendar( + firstDay: kFirstDay, + lastDay: kLastDay, + focusedDay: state.focusedDay, + rowHeight: GridSize.popoverItemHeight, + calendarFormat: state.format, + daysOfWeekHeight: GridSize.popoverItemHeight, + headerStyle: HeaderStyle( + formatButtonVisible: false, + titleCentered: true, + titleTextStyle: textStyle, + leftChevronMargin: EdgeInsets.zero, + leftChevronPadding: EdgeInsets.zero, + leftChevronIcon: svgWidget( + "home/arrow_left", + color: Theme.of(context).iconTheme.color, + ), + rightChevronPadding: EdgeInsets.zero, + rightChevronMargin: EdgeInsets.zero, + rightChevronIcon: svgWidget( + "home/arrow_right", + color: Theme.of(context).iconTheme.color, + ), + headerMargin: const EdgeInsets.only(bottom: 8.0), + ), + daysOfWeekStyle: DaysOfWeekStyle( + dowTextFormatter: (date, locale) => + DateFormat.E(locale).format(date).toUpperCase(), + weekdayStyle: AFThemeExtension.of(context).caption, + weekendStyle: AFThemeExtension.of(context).caption, + ), + calendarStyle: CalendarStyle( + cellMargin: const EdgeInsets.all(3), + defaultDecoration: boxDecoration, + selectedDecoration: boxDecoration.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + todayDecoration: boxDecoration.copyWith( + color: AFThemeExtension.of(context).lightGreyHover, + ), + weekendDecoration: boxDecoration, + outsideDecoration: boxDecoration, + defaultTextStyle: textStyle, + weekendTextStyle: textStyle, + selectedTextStyle: textStyle.copyWith( + color: Theme.of(context).colorScheme.surface, + ), + todayTextStyle: textStyle, + outsideTextStyle: textStyle.copyWith( + color: Theme.of(context).disabledColor, + ), + ), + selectedDayPredicate: (day) => isSameDay(state.dateTime, day), + onDaySelected: (selectedDay, focusedDay) { + context.read().add( + DateCellCalendarEvent.selectDay(selectedDay.toLocal().date), + ); + }, + onFormatChanged: (format) { + context + .read() + .add(DateCellCalendarEvent.setCalFormat(format)); + }, + onPageChanged: (focusedDay) { + context + .read() + .add(DateCellCalendarEvent.setFocusedDay(focusedDay)); + }, + ); + }, + ); + } +} + +class _IncludeTimeButton extends StatelessWidget { + const _IncludeTimeButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.includeTime, + builder: (context, includeTime) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: GridSize.typeOptionContentInsets, + child: Row( + children: [ + svgWidget( + "grid/clock", + color: Theme.of(context).iconTheme.color, + ), + const HSpace(6), + FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()), + const Spacer(), + Toggle( + value: includeTime, + onChanged: (value) => context + .read() + .add(DateCellCalendarEvent.setIncludeTime(!value)), + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class _TimeTextField extends StatefulWidget { + final String? timeStr; + final PopoverMutex popoverMutex; + + const _TimeTextField({ + required this.timeStr, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + State<_TimeTextField> createState() => _TimeTextFieldState(); +} + +class _TimeTextFieldState extends State<_TimeTextField> { + late final FocusNode _focusNode; + late final TextEditingController _textController; + + @override + void initState() { + _focusNode = FocusNode(); + _textController = TextEditingController()..text = widget.timeStr ?? ""; + + _focusNode.addListener(() { + if (_focusNode.hasFocus) { + widget.popoverMutex.close(); + } + }); + + widget.popoverMutex.listenOnPopoverChanged(() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) => _textController.text = state.time ?? "", + builder: (context, state) { + return Column( + children: [ + const VSpace(12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: FlowyTextField( + text: state.time ?? "", + focusNode: _focusNode, + controller: _textController, + submitOnLeave: true, + hintText: state.timeHintText, + errorText: state.timeFormatError, + onSubmitted: (timeStr) { + context + .read() + .add(DateCellCalendarEvent.setTime(timeStr)); + }, + ), + ), + ], + ); + }, + ); + } +} + +class _DateTypeOptionButton extends StatelessWidget { + final PopoverMutex popoverMutex; + const _DateTypeOptionButton({ + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final title = + "${LocaleKeys.grid_field_dateFormat.tr()} & ${LocaleKeys.grid_field_timeFormat.tr()}"; + return BlocSelector( + selector: (state) => state.dateTypeOptionPB, + builder: (context, dateTypeOptionPB) { + return AppFlowyPopover( + mutex: popoverMutex, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + margin: EdgeInsets.zero, + constraints: BoxConstraints.loose(const Size(140, 100)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(title), + margin: GridSize.typeOptionContentInsets, + rightIcon: const FlowySvg(name: 'grid/more'), + ), + ), + ), + popupBuilder: (BuildContext popContext) { + return _CalDateTimeSetting( + dateTypeOptionPB: dateTypeOptionPB, + onEvent: (event) { + context.read().add(event); + popoverMutex.close(); + }, + ); + }, + ); + }, + ); + } +} + +class _CalDateTimeSetting extends StatefulWidget { + final DateTypeOptionPB dateTypeOptionPB; + final Function(DateCellCalendarEvent) onEvent; + const _CalDateTimeSetting({ + required this.dateTypeOptionPB, + required this.onEvent, + Key? key, + }) : super(key: key); + + @override + State<_CalDateTimeSetting> createState() => _CalDateTimeSettingState(); +} + +class _CalDateTimeSettingState extends State<_CalDateTimeSetting> { + final timeSettingPopoverMutex = PopoverMutex(); + String? overlayIdentifier; + + @override + Widget build(BuildContext context) { + final List children = [ + AppFlowyPopover( + mutex: timeSettingPopoverMutex, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + popupBuilder: (BuildContext context) { + return DateFormatList( + selectedFormat: widget.dateTypeOptionPB.dateFormat, + onSelected: (format) { + widget.onEvent(DateCellCalendarEvent.setDateFormat(format)); + timeSettingPopoverMutex.close(); + }, + ); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 6.0), + child: DateFormatButton(), + ), + ), + AppFlowyPopover( + mutex: timeSettingPopoverMutex, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + popupBuilder: (BuildContext context) { + return TimeFormatList( + selectedFormat: widget.dateTypeOptionPB.timeFormat, + onSelected: (format) { + widget.onEvent(DateCellCalendarEvent.setTimeFormat(format)); + timeSettingPopoverMutex.close(); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: + TimeFormatButton(timeFormat: widget.dateTypeOptionPB.timeFormat), + ), + ), + ]; + + return SizedBox( + width: 180, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + padding: const EdgeInsets.symmetric(vertical: 6.0), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell.dart new file mode 100644 index 0000000000..6b85dc3f19 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'number_cell_bloc.dart'; +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../cell_builder.dart'; + +class GridNumberCell extends GridCellWidget { + final CellControllerBuilder cellControllerBuilder; + + GridNumberCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + GridFocusNodeCellState createState() => _NumberCellState(); +} + +class _NumberCellState extends GridFocusNodeCellState { + late NumberCellBloc _cellBloc; + late TextEditingController _controller; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as NumberCellController; + _cellBloc = NumberCellBloc(cellController: cellController) + ..add(const NumberCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.cellContent); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.cellContent != c.cellContent, + listener: (context, state) => _controller.text = state.cellContent, + ), + ], + child: Padding( + padding: GridSize.cellContentInsets, + child: TextField( + controller: _controller, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: 1, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + isDense: true, + ), + ), + ), + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Future focusChanged() async { + if (mounted) { + if (_cellBloc.isClosed == false && + _controller.text != _cellBloc.state.cellContent) { + _cellBloc.add(NumberCellEvent.updateCell(_controller.text)); + } + } + } + + @override + String? onCopy() { + return _cellBloc.state.cellContent; + } + + @override + void onInsert(String value) { + _cellBloc.add(NumberCellEvent.updateCell(value)); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart new file mode 100644 index 0000000000..da152a3e5d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart @@ -0,0 +1,85 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'number_cell_bloc.freezed.dart'; + +// +class NumberCellBloc extends Bloc { + final NumberCellController cellController; + void Function()? _onCellChangedFn; + + NumberCellBloc({ + required this.cellController, + }) : super(NumberCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveCellUpdate: (cellContent) { + emit(state.copyWith(cellContent: cellContent ?? "")); + }, + updateCell: (text) async { + if (state.cellContent != text) { + emit(state.copyWith(cellContent: 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(), + ), + ); + } + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(NumberCellEvent.didReceiveCellUpdate(cellContent)); + } + }), + ); + } +} + +@freezed +class NumberCellEvent with _$NumberCellEvent { + const factory NumberCellEvent.initial() = _Initial; + const factory NumberCellEvent.updateCell(String text) = _UpdateCell; + const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) = + _DidReceiveCellUpdate; +} + +@freezed +class NumberCellState with _$NumberCellState { + const factory NumberCellState({ + required String cellContent, + }) = _NumberCellState; + + factory NumberCellState.initial(TextCellController context) { + return NumberCellState( + cellContent: context.getCellData() ?? "", + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart new file mode 100644 index 0000000000..fe38716d12 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart @@ -0,0 +1,173 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +extension SelectOptionColorExtension on SelectOptionColorPB { + Color toColor(BuildContext context) { + switch (this) { + case SelectOptionColorPB.Purple: + return AFThemeExtension.of(context).tint1; + case SelectOptionColorPB.Pink: + return AFThemeExtension.of(context).tint2; + case SelectOptionColorPB.LightPink: + return AFThemeExtension.of(context).tint3; + case SelectOptionColorPB.Orange: + return AFThemeExtension.of(context).tint4; + case SelectOptionColorPB.Yellow: + return AFThemeExtension.of(context).tint5; + case SelectOptionColorPB.Lime: + return AFThemeExtension.of(context).tint6; + case SelectOptionColorPB.Green: + return AFThemeExtension.of(context).tint7; + case SelectOptionColorPB.Aqua: + return AFThemeExtension.of(context).tint8; + case SelectOptionColorPB.Blue: + return AFThemeExtension.of(context).tint9; + default: + throw ArgumentError; + } + } + + String optionName() { + switch (this) { + case SelectOptionColorPB.Purple: + return LocaleKeys.grid_selectOption_purpleColor.tr(); + case SelectOptionColorPB.Pink: + return LocaleKeys.grid_selectOption_pinkColor.tr(); + case SelectOptionColorPB.LightPink: + return LocaleKeys.grid_selectOption_lightPinkColor.tr(); + case SelectOptionColorPB.Orange: + return LocaleKeys.grid_selectOption_orangeColor.tr(); + case SelectOptionColorPB.Yellow: + return LocaleKeys.grid_selectOption_yellowColor.tr(); + case SelectOptionColorPB.Lime: + return LocaleKeys.grid_selectOption_limeColor.tr(); + case SelectOptionColorPB.Green: + return LocaleKeys.grid_selectOption_greenColor.tr(); + case SelectOptionColorPB.Aqua: + return LocaleKeys.grid_selectOption_aquaColor.tr(); + case SelectOptionColorPB.Blue: + return LocaleKeys.grid_selectOption_blueColor.tr(); + default: + throw ArgumentError; + } + } +} + +class SelectOptionTag extends StatelessWidget { + final String name; + final Color color; + final VoidCallback? onSelected; + final void Function(String)? onRemove; + const SelectOptionTag({ + required this.name, + required this.color, + this.onSelected, + this.onRemove, + Key? key, + }) : super(key: key); + + factory SelectOptionTag.fromOption({ + required BuildContext context, + required SelectOptionPB option, + VoidCallback? onSelected, + Function(String)? onRemove, + }) { + return SelectOptionTag( + name: option.name, + color: option.color.toColor(context), + onSelected: onSelected, + onRemove: onRemove, + ); + } + + @override + Widget build(BuildContext context) { + EdgeInsets padding = + const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0); + if (onRemove != null) { + padding = padding.copyWith(right: 2.0); + } + + return Container( + padding: padding, + decoration: BoxDecoration( + color: color, + borderRadius: Corners.s6Border, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText.medium( + name, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (onRemove != null) + FlowyIconButton( + width: 18.0, + onPressed: () => onRemove?.call(name), + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + icon: svgWidget( + 'home/close', + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + ); + } +} + +class SelectOptionTagCell extends StatelessWidget { + final List children; + final void Function(SelectOptionPB) onSelected; + final SelectOptionPB option; + const SelectOptionTagCell({ + required this.option, + required this.onSelected, + this.children = const [], + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyHover( + style: HoverStyle( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + ), + child: InkWell( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: SelectOptionTag.fromOption( + context: context, + option: option, + onSelected: () => onSelected(option), + ), + ), + ), + ), + ...children, + ], + ), + onTap: () => onSelected(option), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart new file mode 100644 index 0000000000..27dc18b852 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart @@ -0,0 +1,227 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.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 '../../../../grid/presentation/layout/sizes.dart'; +import '../../cell_builder.dart'; +import 'extension.dart'; +import 'select_option_cell_bloc.dart'; +import 'select_option_editor.dart'; + +class SelectOptionCellStyle extends GridCellStyle { + String placeholder; + + SelectOptionCellStyle({ + required this.placeholder, + }); +} + +class GridSingleSelectCell extends GridCellWidget { + final CellControllerBuilder cellControllerBuilder; + late final SelectOptionCellStyle? cellStyle; + + GridSingleSelectCell({ + required this.cellControllerBuilder, + GridCellStyle? style, + Key? key, + }) : super(key: key) { + if (style != null) { + cellStyle = (style as SelectOptionCellStyle); + } else { + cellStyle = null; + } + } + + @override + GridCellState createState() => _SingleSelectCellState(); +} + +class _SingleSelectCellState extends GridCellState { + late SelectOptionCellBloc _cellBloc; + late final PopoverController _popover; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as SelectOptionCellController; + _cellBloc = SelectOptionCellBloc(cellController: cellController) + ..add(const SelectOptionCellEvent.initial()); + _popover = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + return SelectOptionWrap( + selectOptions: state.selectedOptions, + cellStyle: widget.cellStyle, + onCellEditing: widget.onCellEditing, + popoverController: _popover, + cellControllerBuilder: widget.cellControllerBuilder, + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + void requestBeginFocus() => _popover.show(); +} + +//---------------------------------------------------------------- +class GridMultiSelectCell extends GridCellWidget { + final CellControllerBuilder cellControllerBuilder; + late final SelectOptionCellStyle? cellStyle; + + GridMultiSelectCell({ + required this.cellControllerBuilder, + GridCellStyle? style, + Key? key, + }) : super(key: key) { + if (style != null) { + cellStyle = (style as SelectOptionCellStyle); + } else { + cellStyle = null; + } + } + + @override + GridCellState createState() => _MultiSelectCellState(); +} + +class _MultiSelectCellState extends GridCellState { + late SelectOptionCellBloc _cellBloc; + late final PopoverController _popover; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as SelectOptionCellController; + _cellBloc = SelectOptionCellBloc(cellController: cellController) + ..add(const SelectOptionCellEvent.initial()); + _popover = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + return SelectOptionWrap( + selectOptions: state.selectedOptions, + cellStyle: widget.cellStyle, + onCellEditing: widget.onCellEditing, + popoverController: _popover, + cellControllerBuilder: widget.cellControllerBuilder, + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + void requestBeginFocus() => _popover.show(); +} + +class SelectOptionWrap extends StatefulWidget { + final List selectOptions; + final SelectOptionCellStyle? cellStyle; + final CellControllerBuilder cellControllerBuilder; + final PopoverController popoverController; + final ValueNotifier onCellEditing; + + const SelectOptionWrap({ + required this.selectOptions, + required this.cellControllerBuilder, + required this.onCellEditing, + required this.popoverController, + this.cellStyle, + Key? key, + }) : super(key: key); + + @override + State createState() => _SelectOptionWrapState(); +} + +class _SelectOptionWrapState extends State { + @override + Widget build(BuildContext context) { + final Widget child = _buildOptions(context); + + final constraints = BoxConstraints.loose( + Size( + SelectOptionCellEditor.editorPanelWidth, + 300, + ), + ); + return AppFlowyPopover( + controller: widget.popoverController, + constraints: constraints, + margin: EdgeInsets.zero, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onCellEditing.value = true; + }); + return SelectOptionCellEditor( + cellController: widget.cellControllerBuilder.build() + as SelectOptionCellController, + ); + }, + onClose: () => widget.onCellEditing.value = false, + child: Padding( + padding: GridSize.cellContentInsets, + child: child, + ), + ); + } + + Widget _buildOptions(BuildContext context) { + final Widget child; + if (widget.selectOptions.isEmpty && widget.cellStyle != null) { + child = FlowyText.medium( + widget.cellStyle!.placeholder, + color: Theme.of(context).hintColor, + ); + } else { + final children = widget.selectOptions.map( + (option) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SelectOptionTag.fromOption( + context: context, + option: option, + ), + ); + }, + ).toList(); + + child = Wrap( + runSpacing: 4, + children: children, + ); + } + return Align(alignment: Alignment.centerLeft, child: child); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart new file mode 100644 index 0000000000..560f4390a7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'select_option_cell_bloc.freezed.dart'; + +class SelectOptionCellBloc + extends Bloc { + final SelectOptionCellController cellController; + void Function()? _onCellChangedFn; + + SelectOptionCellBloc({ + required this.cellController, + }) : super(SelectOptionCellState.initial(cellController)) { + on( + (event, emit) async { + await event.map( + initial: (_InitialCell value) async { + _startListening(); + }, + didReceiveOptions: (_DidReceiveOptions value) { + emit( + state.copyWith( + selectedOptions: value.selectedOptions, + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((selectOptionContext) { + if (!isClosed) { + add( + SelectOptionCellEvent.didReceiveOptions( + selectOptionContext?.selectOptions ?? [], + ), + ); + } + }), + ); + } +} + +@freezed +class SelectOptionCellEvent with _$SelectOptionCellEvent { + const factory SelectOptionCellEvent.initial() = _InitialCell; + const factory SelectOptionCellEvent.didReceiveOptions( + List selectedOptions, + ) = _DidReceiveOptions; +} + +@freezed +class SelectOptionCellState with _$SelectOptionCellState { + const factory SelectOptionCellState({ + required List selectedOptions, + }) = _SelectOptionCellState; + + factory SelectOptionCellState.initial(SelectOptionCellController context) { + final data = context.getCellData(); + + return SelectOptionCellState( + selectedOptions: data?.selectOptions ?? [], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart new file mode 100644 index 0000000000..d0d573783d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart @@ -0,0 +1,367 @@ +import 'dart:collection'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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:textfield_tags/textfield_tags.dart'; + +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../../../grid/presentation/widgets/common/type_option_separator.dart'; +import '../../../../grid/presentation/widgets/header/type_option/select_option_editor.dart'; +import 'extension.dart'; +import 'select_option_editor_bloc.dart'; +import 'text_field.dart'; + +const double _editorPanelWidth = 300; +const double _padding = 12.0; + +class SelectOptionCellEditor extends StatefulWidget { + final SelectOptionCellController cellController; + static double editorPanelWidth = 300; + + const SelectOptionCellEditor({required this.cellController, Key? key}) + : super(key: key); + + @override + State createState() => _SelectOptionCellEditorState(); +} + +class _SelectOptionCellEditorState extends State { + final popoverMutex = PopoverMutex(); + final tagController = TextfieldTagsController(); + + @override + void dispose() { + popoverMutex.dispose(); + tagController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SelectOptionCellEditorBloc( + cellController: widget.cellController, + )..add(const SelectOptionEditorEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TextField( + popoverMutex: popoverMutex, + tagController: tagController, + ), + const TypeOptionSeparator(spacing: 0.0), + Flexible( + child: _OptionList( + popoverMutex: popoverMutex, + tagController: tagController, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _OptionList extends StatelessWidget { + final PopoverMutex popoverMutex; + final TextfieldTagsController tagController; + + const _OptionList({ + required this.popoverMutex, + required this.tagController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final cells = [ + _Title(onPressedAddButton: () => onPressedAddButton(context)), + ...state.options.map( + (option) => _SelectOptionCell( + option: option, + isSelected: state.selectedOptions.contains(option), + popoverMutex: popoverMutex, + ), + ), + ]; + + state.createOption.fold( + () => null, + (createOption) { + cells.add(_CreateOptionCell(name: createOption)); + }, + ); + + return ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (_, __) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (_, int index) => cells[index], + padding: const EdgeInsets.only(top: 6.0, bottom: 12.0), + ); + }, + ); + } + + void onPressedAddButton(BuildContext context) { + final text = tagController.textEditingController?.text; + if (text != null) { + context.read().add( + SelectOptionEditorEvent.trySelectOption(text), + ); + } + tagController.textEditingController?.clear(); + } +} + +class _TextField extends StatelessWidget { + final PopoverMutex popoverMutex; + final TextfieldTagsController tagController; + + const _TextField({ + required this.popoverMutex, + required this.tagController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final optionMap = LinkedHashMap.fromIterable( + state.selectedOptions, + key: (option) => option.name, + value: (option) => option, + ); + + return Padding( + padding: const EdgeInsets.all(_padding), + child: SelectOptionTextField( + options: state.options, + selectedOptionMap: optionMap, + distanceToText: _editorPanelWidth * 0.7, + maxLength: 30, + tagController: tagController, + textSeparators: const [','], + onClick: () => popoverMutex.close(), + newText: (text) { + context + .read() + .add(SelectOptionEditorEvent.filterOption(text)); + }, + onSubmitted: (tagName) { + context + .read() + .add(SelectOptionEditorEvent.trySelectOption(tagName)); + }, + onPaste: (tagNames, remainder) { + context.read().add( + SelectOptionEditorEvent.selectMultipleOptions( + tagNames, + remainder, + ), + ); + }, + onRemove: (optionName) { + context.read().add( + SelectOptionEditorEvent.unSelectOption( + optionMap[optionName]!.id, + ), + ); + }, + ), + ); + }, + ); + } +} + +class _Title extends StatelessWidget { + const _Title({ + required this.onPressedAddButton, + }); + + final VoidCallback onPressedAddButton; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlowyText.medium( + LocaleKeys.grid_selectOption_panelTitle.tr(), + color: Theme.of(context).hintColor, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + ), + child: FlowyIconButton( + onPressed: onPressedAddButton, + width: 18, + icon: svgWidget( + 'home/add', + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _CreateOptionCell extends StatelessWidget { + const _CreateOptionCell({ + required this.name, + }); + + final String name; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: Row( + children: [ + FlowyText.medium( + LocaleKeys.grid_selectOption_create.tr(), + color: Theme.of(context).hintColor, + ), + const HSpace(10), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: SelectOptionTag( + name: name, + color: AFThemeExtension.of(context).greyHover, + onSelected: () => context + .read() + .add(SelectOptionEditorEvent.newOption(name)), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SelectOptionCell extends StatefulWidget { + final SelectOptionPB option; + final PopoverMutex popoverMutex; + final bool isSelected; + + const _SelectOptionCell({ + required this.option, + required this.isSelected, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + State<_SelectOptionCell> createState() => _SelectOptionCellState(); +} + +class _SelectOptionCellState extends State<_SelectOptionCell> { + late PopoverController _popoverController; + + @override + void initState() { + _popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final child = SizedBox( + height: GridSize.popoverItemHeight, + child: SelectOptionTagCell( + option: widget.option, + onSelected: (option) { + if (widget.isSelected) { + context + .read() + .add(SelectOptionEditorEvent.unSelectOption(option.id)); + } else { + context + .read() + .add(SelectOptionEditorEvent.selectOption(option.id)); + } + }, + children: [ + if (widget.isSelected) + Padding( + padding: const EdgeInsets.only(left: 6), + child: svgWidget("grid/checkmark"), + ), + FlowyIconButton( + onPressed: () => _popoverController.show(), + hoverColor: Colors.transparent, + iconPadding: const EdgeInsets.symmetric(horizontal: 6.0), + icon: svgWidget( + "editor/details", + color: Theme.of(context).iconTheme.color, + ), + ), + ], + ), + ); + return AppFlowyPopover( + controller: _popoverController, + offset: const Offset(8, 0), + margin: EdgeInsets.zero, + asBarrier: true, + constraints: BoxConstraints.loose(const Size(200, 470)), + mutex: widget.popoverMutex, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: child, + ), + popupBuilder: (BuildContext popoverContext) { + return SelectOptionTypeOptionEditor( + option: widget.option, + onDeleted: () { + context + .read() + .add(SelectOptionEditorEvent.deleteOption(widget.option)); + PopoverContainer.of(popoverContext).close(); + }, + onUpdated: (updatedOption) { + context + .read() + .add(SelectOptionEditorEvent.updateOption(updatedOption)); + }, + key: ValueKey( + widget.option.id, + ), // Use ValueKey to refresh the UI, otherwise, it will remain the old value. + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart new file mode 100644 index 0000000000..71231a31d7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart @@ -0,0 +1,293 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../../../application/cell/select_option_cell_service.dart'; + +part 'select_option_editor_bloc.freezed.dart'; + +class SelectOptionCellEditorBloc + extends Bloc { + final SelectOptionCellBackendService _selectOptionService; + final SelectOptionCellController cellController; + + SelectOptionCellEditorBloc({ + required this.cellController, + }) : _selectOptionService = SelectOptionCellBackendService( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + rowId: cellController.rowId, + ), + super(SelectOptionEditorState.initial(cellController)) { + on( + (event, emit) async { + await event.map( + initial: (_Initial value) async { + _startListening(); + await _loadOptions(); + }, + didReceiveOptions: (_DidReceiveOptions value) { + final result = _makeOptions(state.filter, value.options); + emit( + state.copyWith( + allOptions: value.options, + options: result.options, + createOption: result.createOption, + selectedOptions: value.selectedOptions, + ), + ); + }, + newOption: (_NewOption value) async { + await _createOption(value.optionName); + emit( + state.copyWith( + filter: none(), + ), + ); + }, + deleteOption: (_DeleteOption value) async { + await _deleteOption([value.option]); + }, + deleteAllOptions: (_DeleteAllOptions value) async { + if (state.allOptions.isNotEmpty) { + await _deleteOption(state.allOptions); + } + }, + updateOption: (_UpdateOption value) async { + await _updateOption(value.option); + }, + selectOption: (_SelectOption value) async { + await _selectOptionService.select(optionIds: [value.optionId]); + }, + unSelectOption: (_UnSelectOption value) async { + await _selectOptionService.unSelect(optionIds: [value.optionId]); + }, + trySelectOption: (_TrySelectOption value) { + _trySelectOption(value.optionName, emit); + }, + selectMultipleOptions: (_SelectMultipleOptions value) { + if (value.optionNames.isNotEmpty) { + _selectMultipleOptions(value.optionNames); + } + _filterOption(value.remainder, emit); + }, + filterOption: (_SelectOptionFilter value) { + _filterOption(value.optionName, emit); + }, + ); + }, + ); + } + + @override + Future close() async { + await cellController.dispose(); + return super.close(); + } + + Future _createOption(String name) async { + final result = await _selectOptionService.create(name: name); + result.fold((l) => {}, (err) => Log.error(err)); + } + + Future _deleteOption(List options) async { + final result = await _selectOptionService.delete(options: options); + result.fold((l) => null, (err) => Log.error(err)); + } + + Future _updateOption(SelectOptionPB option) async { + final result = await _selectOptionService.update( + option: option, + ); + + result.fold((l) => null, (err) => Log.error(err)); + } + + void _trySelectOption( + String optionName, + Emitter emit, + ) { + SelectOptionPB? matchingOption; + bool optionExistsButSelected = false; + + for (final option in state.options) { + if (option.name.toLowerCase() == optionName.toLowerCase()) { + if (!state.selectedOptions.contains(option)) { + matchingOption = option; + break; + } else { + optionExistsButSelected = true; + } + } + } + + // if there isn't a matching option at all, then create it + if (matchingOption == null && !optionExistsButSelected) { + _createOption(optionName); + } + + // if there is an unselected matching option, select it + if (matchingOption != null) { + _selectOptionService.select(optionIds: [matchingOption.id]); + } + + // clear the filter + emit(state.copyWith(filter: none())); + } + + void _selectMultipleOptions(List optionNames) { + // The options are unordered. So in order to keep the inserted [optionNames] + // order, it needs to get the option id in the [optionNames] order. + final lowerCaseNames = optionNames.map((e) => e.toLowerCase()); + final Map optionIdsMap = {}; + for (final option in state.options) { + optionIdsMap[option.name.toLowerCase()] = option.id; + } + + final optionIds = lowerCaseNames + .where((name) => optionIdsMap[name] != null) + .map((name) => optionIdsMap[name]!) + .toList(); + + _selectOptionService.select(optionIds: optionIds); + } + + void _filterOption(String optionName, Emitter emit) { + final _MakeOptionResult result = _makeOptions( + Some(optionName), + state.allOptions, + ); + emit( + state.copyWith( + filter: Some(optionName), + options: result.options, + createOption: result.createOption, + ), + ); + } + + Future _loadOptions() async { + final result = await _selectOptionService.getCellData(); + if (isClosed) { + Log.warn("Unexpected closing the bloc"); + return; + } + + return result.fold( + (data) => add( + SelectOptionEditorEvent.didReceiveOptions( + data.options, + data.selectOptions, + ), + ), + (err) { + Log.error(err); + return null; + }, + ); + } + + _MakeOptionResult _makeOptions( + Option filter, + List allOptions, + ) { + final List options = List.from(allOptions); + Option createOption = filter; + + filter.foldRight(null, (filter, previous) { + if (filter.isNotEmpty) { + options.retainWhere((option) { + final name = option.name.toLowerCase(); + final lFilter = filter.toLowerCase(); + + if (name == lFilter) { + createOption = none(); + } + + return name.contains(lFilter); + }); + } else { + createOption = none(); + } + }); + + return _MakeOptionResult( + options: options, + createOption: createOption, + ); + } + + void _startListening() { + cellController.startListening( + onCellChanged: ((selectOptionContext) { + _loadOptions(); + }), + onCellFieldChanged: () { + _loadOptions(); + }, + ); + } +} + +@freezed +class SelectOptionEditorEvent with _$SelectOptionEditorEvent { + const factory SelectOptionEditorEvent.initial() = _Initial; + const factory SelectOptionEditorEvent.didReceiveOptions( + List options, + List selectedOptions, + ) = _DidReceiveOptions; + const factory SelectOptionEditorEvent.newOption(String optionName) = + _NewOption; + const factory SelectOptionEditorEvent.selectOption(String optionId) = + _SelectOption; + const factory SelectOptionEditorEvent.unSelectOption(String optionId) = + _UnSelectOption; + const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) = + _UpdateOption; + const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) = + _DeleteOption; + const factory SelectOptionEditorEvent.deleteAllOptions() = _DeleteAllOptions; + const factory SelectOptionEditorEvent.filterOption(String optionName) = + _SelectOptionFilter; + const factory SelectOptionEditorEvent.trySelectOption(String optionName) = + _TrySelectOption; + const factory SelectOptionEditorEvent.selectMultipleOptions( + List optionNames, + String remainder, + ) = _SelectMultipleOptions; +} + +@freezed +class SelectOptionEditorState with _$SelectOptionEditorState { + const factory SelectOptionEditorState({ + required List options, + required List allOptions, + required List selectedOptions, + required Option createOption, + required Option filter, + }) = _SelectOptionEditorState; + + factory SelectOptionEditorState.initial(SelectOptionCellController context) { + final data = context.getCellData(loadIfNotExist: false); + return SelectOptionEditorState( + options: data?.options ?? [], + allOptions: data?.options ?? [], + selectedOptions: data?.selectOptions ?? [], + createOption: none(), + filter: none(), + ); + } +} + +class _MakeOptionResult { + List options; + Option createOption; + + _MakeOptionResult({ + required this.options, + required this.createOption, + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart new file mode 100644 index 0000000000..c0ef257dc0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart @@ -0,0 +1,223 @@ +import 'dart:collection'; + +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flutter/services.dart'; +import 'package:textfield_tags/textfield_tags.dart'; + +import 'extension.dart'; + +class SelectOptionTextField extends StatefulWidget { + final TextfieldTagsController tagController; + final List options; + final LinkedHashMap selectedOptionMap; + final double distanceToText; + final List textSeparators; + + final Function(String) onSubmitted; + final Function(String) newText; + final Function(List, String) onPaste; + final Function(String) onRemove; + final VoidCallback? onClick; + final int? maxLength; + + const SelectOptionTextField({ + required this.options, + required this.selectedOptionMap, + required this.distanceToText, + required this.tagController, + required this.onSubmitted, + required this.onPaste, + required this.onRemove, + required this.newText, + required this.textSeparators, + this.onClick, + this.maxLength, + TextEditingController? textController, + FocusNode? focusNode, + Key? key, + }) : super(key: key); + + @override + State createState() => _SelectOptionTextFieldState(); +} + +class _SelectOptionTextFieldState extends State { + late FocusNode focusNode; + late TextEditingController controller; + + @override + void initState() { + focusNode = FocusNode(); + controller = TextEditingController(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + super.initState(); + } + + String? _suffixText() { + if (widget.maxLength != null) { + return ' ${controller.text.length}/${widget.maxLength}'; + } else { + return null; + } + } + + @override + Widget build(BuildContext context) { + return TextFieldTags( + textEditingController: controller, + textfieldTagsController: widget.tagController, + initialTags: widget.selectedOptionMap.keys.toList(), + focusNode: focusNode, + textSeparators: widget.textSeparators, + inputfieldBuilder: ( + BuildContext context, + editController, + focusNode, + error, + onChanged, + onSubmitted, + ) { + return ((context, sc, tags, onTagDelegate) { + return TextField( + controller: editController, + focusNode: focusNode, + onTap: widget.onClick, + onChanged: (text) { + if (onChanged != null) { + onChanged(text); + } + _newText(text, editController); + }, + onSubmitted: (text) { + if (onSubmitted != null) { + onSubmitted(text); + } + + if (text.isNotEmpty) { + widget.onSubmitted(text.trim()); + focusNode.requestFocus(); + } + }, + maxLines: 1, + maxLength: widget.maxLength, + maxLengthEnforcement: + MaxLengthEnforcement.truncateAfterCompositionEnds, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + width: 1.0, + ), + borderRadius: Corners.s10Border, + ), + isDense: true, + prefixIcon: _renderTags(context, sc), + hintText: LocaleKeys.grid_selectOption_searchOption.tr(), + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).hintColor), + suffixText: _suffixText(), + counterText: "", + prefixIconConstraints: + BoxConstraints(maxWidth: widget.distanceToText), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 1.0, + ), + borderRadius: Corners.s10Border, + ), + ), + ); + }); + }, + ); + } + + void _newText(String text, TextEditingController editingController) { + if (text.isEmpty) { + widget.newText(''); + return; + } + + final result = splitInput(text.trimLeft(), widget.textSeparators); + + editingController.text = result[1]; + editingController.selection = + TextSelection.collapsed(offset: controller.text.length); + widget.onPaste(result[0], result[1]); + } + + Widget? _renderTags(BuildContext context, ScrollController sc) { + if (widget.selectedOptionMap.isEmpty) { + return null; + } + + final children = widget.selectedOptionMap.values + .map( + (option) => SelectOptionTag.fromOption( + context: context, + option: option, + onRemove: (option) => widget.onRemove(option), + ), + ) + .toList(); + return MouseRegion( + cursor: SystemMouseCursors.basic, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.trackpad, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, + }, + ), + child: SingleChildScrollView( + controller: sc, + scrollDirection: Axis.horizontal, + child: Wrap(spacing: 4, children: children), + ), + ), + ), + ); + } +} + +@visibleForTesting +List splitInput(String input, List textSeparators) { + final List splits = []; + String currentString = ''; + + // split the string into tokens + for (final char in input.split('')) { + if (textSeparators.contains(char)) { + if (currentString.trim().isNotEmpty) { + splits.add(currentString.trim()); + } + currentString = ''; + continue; + } + currentString += char; + } + // add the remainder (might be '') + splits.add(currentString); + + final submittedOptions = splits.sublist(0, splits.length - 1).toList(); + final remainder = splits.elementAt(splits.length - 1).trimLeft(); + + return [submittedOptions, remainder]; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart new file mode 100644 index 0000000000..4fc99b9bfe --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart @@ -0,0 +1,137 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/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'; +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../cell_builder.dart'; + +class GridTextCellStyle extends GridCellStyle { + String? placeholder; + TextStyle? textStyle; + bool? autofocus; + double emojiFontSize; + double emojiHPadding; + bool showEmoji; + + GridTextCellStyle({ + this.placeholder, + this.textStyle, + this.autofocus, + this.showEmoji = true, + this.emojiFontSize = 16, + this.emojiHPadding = 0, + }); +} + +class GridTextCell extends GridCellWidget { + final CellControllerBuilder cellControllerBuilder; + late final GridTextCellStyle cellStyle; + GridTextCell({ + required this.cellControllerBuilder, + GridCellStyle? style, + Key? key, + }) : super(key: key) { + if (style != null) { + cellStyle = (style as GridTextCellStyle); + } else { + cellStyle = GridTextCellStyle(); + } + } + + @override + GridFocusNodeCellState createState() => _GridTextCellState(); +} + +class _GridTextCellState extends GridFocusNodeCellState { + late TextCellBloc _cellBloc; + late TextEditingController _controller; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as TextCellController; + _cellBloc = TextCellBloc(cellController: cellController); + _cellBloc.add(const TextCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocListener( + listener: (context, state) { + if (_controller.text != state.content) { + _controller.text = state.content; + } + }, + child: Padding( + padding: EdgeInsets.only( + left: GridSize.cellContentInsets.left, + right: GridSize.cellContentInsets.right, + ), + child: Row( + children: [ + if (widget.cellStyle.showEmoji) + // Only build the emoji when it changes + BlocBuilder( + buildWhen: (p, c) => p.emoji != c.emoji, + builder: (context, state) => Center( + child: FlowyText( + state.emoji, + fontSize: widget.cellStyle.emojiFontSize, + ), + ), + ), + HSpace(widget.cellStyle.emojiHPadding), + Expanded( + child: TextField( + controller: _controller, + focusNode: focusNode, + maxLines: null, + style: widget.cellStyle.textStyle ?? + Theme.of(context).textTheme.bodyMedium, + autofocus: widget.cellStyle.autofocus ?? false, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + top: GridSize.cellContentInsets.top, + bottom: GridSize.cellContentInsets.bottom, + ), + border: InputBorder.none, + hintText: widget.cellStyle.placeholder, + isDense: true, + ), + ), + ) + ], + ), + ), + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + String? onCopy() => _cellBloc.state.content; + + @override + void onInsert(String value) { + _cellBloc.add(TextCellEvent.updateText(value)); + } + + @override + Future focusChanged() { + _cellBloc.add( + TextCellEvent.updateText(_controller.text), + ); + return super.focusChanged(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart new file mode 100644 index 0000000000..987232a1c4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart @@ -0,0 +1,83 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'text_cell_bloc.freezed.dart'; + +class TextCellBloc extends Bloc { + final TextCellController cellController; + void Function()? _onCellChangedFn; + TextCellBloc({ + required this.cellController, + }) : super(TextCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + updateText: (text) { + if (state.content != text) { + cellController.saveCellData(text); + emit(state.copyWith(content: text)); + } + }, + didReceiveCellUpdate: (content) { + emit(state.copyWith(content: content)); + }, + didUpdateEmoji: (String emoji) { + emit(state.copyWith(emoji: emoji)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(TextCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + onRowMetaChanged: () { + if (!isClosed) { + add(TextCellEvent.didUpdateEmoji(cellController.emoji ?? "")); + } + }, + ); + } +} + +@freezed +class TextCellEvent with _$TextCellEvent { + const factory TextCellEvent.initial() = _InitialCell; + const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; + const factory TextCellEvent.updateText(String text) = _UpdateText; + const factory TextCellEvent.didUpdateEmoji(String emoji) = _UpdateEmoji; +} + +@freezed +class TextCellState with _$TextCellState { + const factory TextCellState({ + required String content, + required String emoji, + }) = _TextCellState; + + factory TextCellState.initial(TextCellController context) => TextCellState( + content: context.getCellData() ?? "", + emoji: context.emoji ?? "", + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/cell_editor.dart new file mode 100644 index 0000000000..a6270ab8d7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/cell_editor.dart @@ -0,0 +1,105 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'url_cell_editor_bloc.dart'; + +class URLCellEditor extends StatefulWidget { + final VoidCallback onExit; + final URLCellController cellController; + const URLCellEditor({ + required this.cellController, + required this.onExit, + Key? key, + }) : super(key: key); + + @override + State createState() => _URLCellEditorState(); +} + +class _URLCellEditorState extends State { + late URLCellEditorBloc _cellBloc; + late TextEditingController _controller; + + @override + void initState() { + _cellBloc = URLCellEditorBloc(cellController: widget.cellController); + _cellBloc.add(const URLCellEditorEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocListener( + listener: (context, state) { + if (_controller.text != state.content) { + _controller.text = state.content; + } + + if (state.isFinishEditing) { + widget.onExit(); + } + }, + child: TextField( + autofocus: true, + controller: _controller, + onSubmitted: (value) => focusChanged(), + onEditingComplete: () => focusChanged(), + maxLines: 1, + style: Theme.of(context).textTheme.bodyMedium, + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + hintText: "", + isDense: true, + ), + ), + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + void focusChanged() { + if (mounted) { + if (_cellBloc.isClosed == false && + _controller.text != _cellBloc.state.content) { + _cellBloc.add(URLCellEditorEvent.updateText(_controller.text)); + } + } + } +} + +class URLEditorPopover extends StatelessWidget { + final VoidCallback onExit; + final URLCellController cellController; + const URLEditorPopover({ + required this.cellController, + required this.onExit, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(4), + ), + padding: const EdgeInsets.all(12), + child: URLCellEditor( + cellController: cellController, + onExit: onExit, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart new file mode 100644 index 0000000000..e22c06b2de --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart @@ -0,0 +1,348 @@ +import 'dart:async'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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:url_launcher/url_launcher_string.dart'; +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../accessory/cell_accessory.dart'; +import '../../cell_builder.dart'; +import 'cell_editor.dart'; +import 'url_cell_bloc.dart'; + +class GridURLCellStyle extends GridCellStyle { + String? placeholder; + TextStyle? textStyle; + bool? autofocus; + + List accessoryTypes; + + GridURLCellStyle({ + this.placeholder, + this.accessoryTypes = const [], + }); +} + +enum GridURLCellAccessoryType { + copyURL, + visitURL, +} + +typedef URLCellDataNotifier = CellDataNotifier; + +class GridURLCell extends GridCellWidget { + GridURLCell({ + super.key, + required this.cellControllerBuilder, + GridCellStyle? style, + }) : _cellDataNotifier = CellDataNotifier(value: '') { + if (style != null) { + cellStyle = (style as GridURLCellStyle); + } else { + cellStyle = null; + } + } + + /// Use + final URLCellDataNotifier _cellDataNotifier; + final CellControllerBuilder cellControllerBuilder; + late final GridURLCellStyle? cellStyle; + + @override + GridCellState createState() => _GridURLCellState(); + + @override + List Function( + GridCellAccessoryBuildContext buildContext, + ) get accessoryBuilder => (buildContext) { + final List accessories = []; + if (cellStyle != null) { + accessories.addAll( + cellStyle!.accessoryTypes.map((ty) { + return _accessoryFromType(ty, buildContext); + }), + ); + } + + // If the accessories is empty then the default accessory will be GridURLCellAccessoryType.visitURL + if (accessories.isEmpty) { + accessories.add( + _accessoryFromType( + GridURLCellAccessoryType.visitURL, + buildContext, + ), + ); + } + + return accessories; + }; + + GridCellAccessoryBuilder _accessoryFromType( + GridURLCellAccessoryType ty, + GridCellAccessoryBuildContext buildContext, + ) { + switch (ty) { + case GridURLCellAccessoryType.visitURL: + return VisitURLCellAccessoryBuilder( + builder: (Key key) => _VisitURLAccessory( + key: key, + cellDataNotifier: _cellDataNotifier, + ), + ); + case GridURLCellAccessoryType.copyURL: + return CopyURLCellAccessoryBuilder( + builder: (Key key) => _CopyURLAccessory( + key: key, + cellDataNotifier: _cellDataNotifier, + ), + ); + } + } +} + +class _GridURLCellState extends GridFocusNodeCellState { + final _popoverController = PopoverController(); + late final URLCellBloc _cellBloc; + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + + final cellController = + widget.cellControllerBuilder.build() as URLCellController; + _cellBloc = URLCellBloc(cellController: cellController) + ..add(const URLCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocConsumer( + listenWhen: (previous, current) => previous.content != current.content, + listener: (context, state) { + _controller.text = state.content; + }, + builder: (context, state) { + widget._cellDataNotifier.value = state.content; + final urlEditor = Padding( + padding: EdgeInsets.only( + left: GridSize.cellContentInsets.left, + right: GridSize.cellContentInsets.right, + ), + child: TextField( + controller: _controller, + focusNode: focusNode, + maxLines: 1, + style: (widget.cellStyle?.textStyle ?? + Theme.of(context).textTheme.bodyMedium) + ?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + autofocus: false, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + top: GridSize.cellContentInsets.top, + bottom: GridSize.cellContentInsets.bottom, + ), + border: InputBorder.none, + hintText: widget.cellStyle?.placeholder, + isDense: true, + ), + ), + ); + return urlEditor; + }, + ), + ); + } + + @override + Future focusChanged() async { + _cellBloc.add(URLCellEvent.updateURL(_controller.text)); + return super.focusChanged(); + } + + @override + void requestBeginFocus() { + widget.onCellEditing.value = true; + _popoverController.show(); + } + + @override + String? onCopy() => _cellBloc.state.content; + + @override + void onInsert(String value) => _cellBloc.add(URLCellEvent.updateURL(value)); +} + +class _EditURLAccessory extends StatefulWidget { + const _EditURLAccessory({ + required this.cellControllerBuilder, + required this.anchorContext, + }); + + final CellControllerBuilder cellControllerBuilder; + final BuildContext anchorContext; + + @override + State createState() => _EditURLAccessoryState(); +} + +class _EditURLAccessoryState extends State<_EditURLAccessory> + with GridCellAccessoryState { + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + margin: EdgeInsets.zero, + constraints: BoxConstraints.loose(const Size(300, 160)), + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 8), + child: svgWidget( + "editor/edit", + color: AFThemeExtension.of(context).textColor, + ), + popupBuilder: (BuildContext popoverContext) { + return URLEditorPopover( + cellController: + widget.cellControllerBuilder.build() as URLCellController, + onExit: () => popoverController.close(), + ); + }, + ); + } + + @override + void onTap() { + popoverController.show(); + } +} + +typedef CopyURLCellAccessoryBuilder + = GridCellAccessoryBuilder>; + +class _CopyURLAccessory extends StatefulWidget { + const _CopyURLAccessory({ + super.key, + required this.cellDataNotifier, + }); + + final URLCellDataNotifier cellDataNotifier; + + @override + State<_CopyURLAccessory> createState() => _CopyURLAccessoryState(); +} + +class _CopyURLAccessoryState extends State<_CopyURLAccessory> + with GridCellAccessoryState { + @override + Widget build(BuildContext context) { + if (widget.cellDataNotifier.value.isNotEmpty) { + return _URLAccessoryIconContainer( + child: svgWidget( + "editor/copy", + color: AFThemeExtension.of(context).textColor, + ), + ); + } else { + return const SizedBox.shrink(); + } + } + + @override + void onTap() { + final content = widget.cellDataNotifier.value; + if (content.isEmpty) { + return; + } + Clipboard.setData(ClipboardData(text: content)); + showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + } +} + +typedef VisitURLCellAccessoryBuilder + = GridCellAccessoryBuilder>; + +class _VisitURLAccessory extends StatefulWidget { + const _VisitURLAccessory({ + super.key, + required this.cellDataNotifier, + }); + + final URLCellDataNotifier cellDataNotifier; + + @override + State<_VisitURLAccessory> createState() => _VisitURLAccessoryState(); +} + +class _VisitURLAccessoryState extends State<_VisitURLAccessory> + with GridCellAccessoryState { + @override + Widget build(BuildContext context) { + if (widget.cellDataNotifier.value.isNotEmpty) { + return _URLAccessoryIconContainer( + child: svgWidget( + "editor/link", + color: AFThemeExtension.of(context).textColor, + ), + ); + } else { + return const SizedBox.shrink(); + } + } + + @override + bool enable() { + return widget.cellDataNotifier.value.isNotEmpty; + } + + @override + void onTap() { + final content = widget.cellDataNotifier.value; + if (content.isEmpty) { + return; + } + final shouldAddScheme = + !['http', 'https'].any((pattern) => content.startsWith(pattern)); + final url = shouldAddScheme ? 'http://$content' : content; + canLaunchUrlString(url).then((value) => launchUrlString(url)); + } +} + +class _URLAccessoryIconContainer extends StatelessWidget { + const _URLAccessoryIconContainer({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 26, + height: 26, + child: Padding( + padding: const EdgeInsets.all(3.0), + child: child, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart new file mode 100644 index 0000000000..f3eb81160e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'url_cell_bloc.freezed.dart'; + +class URLCellBloc extends Bloc { + final URLCellController cellController; + void Function()? _onCellChangedFn; + URLCellBloc({ + required this.cellController, + }) : super(URLCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit( + state.copyWith( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ), + ); + }, + updateURL: (String url) { + cellController.saveCellData(url, deduplicate: true); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(URLCellEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class URLCellEvent with _$URLCellEvent { + const factory URLCellEvent.initial() = _InitialCell; + const factory URLCellEvent.updateURL(String url) = _UpdateURL; + const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; +} + +@freezed +class URLCellState with _$URLCellState { + const factory URLCellState({ + required String content, + required String url, + }) = _URLCellState; + + factory URLCellState.initial(URLCellController context) { + final cellData = context.getCellData(); + return URLCellState( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_editor_bloc.dart new file mode 100644 index 0000000000..c57a1c4093 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_editor_bloc.dart @@ -0,0 +1,81 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'url_cell_editor_bloc.freezed.dart'; + +class URLCellEditorBloc extends Bloc { + final URLCellController cellController; + void Function()? _onCellChangedFn; + URLCellEditorBloc({ + required this.cellController, + }) : super(URLCellEditorState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () { + _startListening(); + }, + updateText: (text) async { + await cellController.saveCellData(text); + emit( + state.copyWith( + content: text, + isFinishEditing: true, + ), + ); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith(content: cellData?.content ?? "")); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(URLCellEditorEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class URLCellEditorEvent with _$URLCellEditorEvent { + const factory URLCellEditorEvent.initial() = _InitialCell; + const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; + const factory URLCellEditorEvent.updateText(String text) = _UpdateText; +} + +@freezed +class URLCellEditorState with _$URLCellEditorState { + const factory URLCellEditorState({ + required String content, + required bool isFinishEditing, + }) = _URLCellEditorState; + + factory URLCellEditorState.initial(URLCellController context) { + final cellData = context.getCellData(); + return URLCellEditorState( + content: cellData?.content ?? "", + isFinishEditing: true, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart new file mode 100644 index 0000000000..e9bc70b5a3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart @@ -0,0 +1,174 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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 RowActionList extends StatelessWidget { + final RowController rowController; + const RowActionList({ + required String viewId, + required this.rowController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: FlowyText(LocaleKeys.grid_row_action.tr()), + ), + const VSpace(15), + RowDetailPageDeleteButton(rowId: rowController.rowId), + RowDetailPageDuplicateButton( + rowId: rowController.rowId, + groupId: rowController.groupId, + ), + ], + ); + } +} + +class RowDetailPageDeleteButton extends StatelessWidget { + final String rowId; + const RowDetailPageDeleteButton({required this.rowId, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()), + leftIcon: const FlowySvg(name: "home/trash"), + onTap: () { + context.read().add(RowDetailEvent.deleteRow(rowId)); + FlowyOverlay.pop(context); + }, + ), + ); + } +} + +class RowDetailPageDuplicateButton extends StatelessWidget { + final String rowId; + final String? groupId; + const RowDetailPageDuplicateButton({ + required this.rowId, + this.groupId, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()), + leftIcon: const FlowySvg(name: "grid/duplicate"), + onTap: () { + context + .read() + .add(RowDetailEvent.duplicateRow(rowId, groupId)); + FlowyOverlay.pop(context); + }, + ), + ); + } +} + +class CreateRowFieldButton extends StatefulWidget { + final String viewId; + + const CreateRowFieldButton({ + required this.viewId, + Key? key, + }) : super(key: key); + + @override + State createState() => _CreateRowFieldButtonState(); +} + +class _CreateRowFieldButtonState extends State { + late PopoverController popoverController; + late TypeOptionPB typeOption; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(240, 200)), + controller: popoverController, + direction: PopoverDirection.topWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + child: SizedBox( + height: 40, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_field_newProperty.tr(), + color: AFThemeExtension.of(context).textColor, + ), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: () async { + final result = await TypeOptionBackendService.createFieldTypeOption( + viewId: widget.viewId, + ); + result.fold( + (l) { + typeOption = l; + popoverController.show(); + }, + (r) => Log.error("Failed to create field type option: $r"), + ); + }, + leftIcon: svgWidget( + "home/add", + color: AFThemeExtension.of(context).textColor, + ), + ), + ), + popupBuilder: (BuildContext popOverContext) { + return FieldEditor( + viewId: widget.viewId, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.viewId, + field: typeOption.field_2, + ), + onDeleted: (fieldId) { + popoverController.close(); + NavigatorAlertDialog( + title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + confirm: () { + context + .read() + .add(RowDetailEvent.deleteField(fieldId)); + }, + ).show(context); + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart new file mode 100644 index 0000000000..a52cf1d045 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart @@ -0,0 +1,268 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.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:flutter_bloc/flutter_bloc.dart'; + +typedef RowBannerCellBuilder = Widget Function(String fieldId); + +class RowBanner extends StatefulWidget { + final String viewId; + final RowMetaPB rowMeta; + final RowBannerCellBuilder cellBuilder; + const RowBanner({ + required this.viewId, + required this.rowMeta, + required this.cellBuilder, + super.key, + }); + + @override + State createState() => _RowBannerState(); +} + +class _RowBannerState extends State { + final _isHovering = ValueNotifier(false); + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RowBannerBloc( + viewId: widget.viewId, + rowMeta: widget.rowMeta, + )..add(const RowBannerEvent.initial()), + child: MouseRegion( + onEnter: (event) => _isHovering.value = true, + onExit: (event) => _isHovering.value = false, + child: SizedBox( + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 30, + child: _BannerAction( + isHovering: _isHovering, + popoverController: popoverController, + ), + ), + _BannerTitle( + cellBuilder: widget.cellBuilder, + popoverController: popoverController, + ), + ], + ), + ), + ), + ); + } +} + +class _BannerAction extends StatelessWidget { + final ValueNotifier isHovering; + final PopoverController popoverController; + const _BannerAction({ + required this.isHovering, + required this.popoverController, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: isHovering, + builder: (BuildContext context, bool value, Widget? child) { + if (value) { + return BlocBuilder( + builder: (context, state) { + final children = []; + final rowMeta = state.rowMeta; + if (rowMeta.icon.isEmpty) { + children.add( + EmojiPickerButton( + showEmojiPicker: () => popoverController.show(), + ), + ); + } else { + children.add( + RemoveEmojiButton( + onRemoved: () { + context + .read() + .add(const RowBannerEvent.setIcon('')); + }, + ), + ); + } + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + }, + ); + } else { + return const SizedBox(height: _kBannerActionHeight); + } + }, + ); + } +} + +class _BannerTitle extends StatefulWidget { + final RowBannerCellBuilder cellBuilder; + final PopoverController popoverController; + const _BannerTitle({ + required this.cellBuilder, + required this.popoverController, + }); + + @override + State<_BannerTitle> createState() => _BannerTitleState(); +} + +class _BannerTitleState extends State<_BannerTitle> { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final children = []; + + if (state.rowMeta.icon.isNotEmpty) { + children.add( + EmojiButton( + emoji: state.rowMeta.icon, + showEmojiPicker: () => widget.popoverController.show(), + ), + ); + } + + if (state.primaryField != null) { + children.add( + Expanded( + child: widget.cellBuilder(state.primaryField!.id), + ), + ); + } + + return AppFlowyPopover( + controller: widget.popoverController, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) { + context + .read() + .add(RowBannerEvent.setIcon(emoji.emoji)); + widget.popoverController.close(); + }), + child: Row(children: children), + ); + }, + ); + } +} + +typedef OnSubmittedEmoji = void Function(Emoji emoji); +const _kBannerActionHeight = 40.0; + +class EmojiButton extends StatelessWidget { + final String emoji; + final VoidCallback showEmojiPicker; + + const EmojiButton({ + required this.emoji, + required this.showEmojiPicker, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: _kBannerActionHeight, + width: _kBannerActionHeight, + child: FlowyButton( + margin: EdgeInsets.zero, + text: FlowyText.medium( + emoji, + fontSize: 30, + textAlign: TextAlign.center, + ), + onTap: showEmojiPicker, + ), + ); + } +} + +class EmojiPickerButton extends StatefulWidget { + final VoidCallback showEmojiPicker; + const EmojiPickerButton({ + super.key, + required this.showEmojiPicker, + }); + + @override + State createState() => _EmojiPickerButtonState(); +} + +class _EmojiPickerButtonState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 160, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ), + leftIcon: const Icon( + Icons.emoji_emotions, + size: 16, + ), + onTap: widget.showEmojiPicker, + ), + ); + } +} + +class RemoveEmojiButton extends StatelessWidget { + final VoidCallback onRemoved; + RemoveEmojiButton({ + super.key, + required this.onRemoved, + }); + + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 160, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + ), + leftIcon: const Icon( + Icons.emoji_emotions, + size: 16, + ), + onTap: onRemoved, + ), + ); + } +} + +Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) { + return SizedBox( + height: 250, + child: EmojiSelectionMenu( + onSubmitted: onSubmitted, + onExit: () {}, + ), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart new file mode 100644 index 0000000000..93eb9582db --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -0,0 +1,171 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_document.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'; + +import 'cell_builder.dart'; +import 'cells/text_cell/text_cell.dart'; +import 'row_action.dart'; +import 'row_banner.dart'; +import 'row_property.dart'; + +class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { + final RowController rowController; + final GridCellBuilder cellBuilder; + + const RowDetailPage({ + required this.rowController, + required this.cellBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _RowDetailPageState(); + + static String identifier() { + return (RowDetailPage).toString(); + } +} + +class _RowDetailPageState extends State { + final scrollController = ScrollController(); + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FlowyDialog( + child: BlocProvider( + create: (context) { + return RowDetailBloc(dataController: widget.rowController) + ..add(const RowDetailEvent.initial()); + }, + child: ListView( + controller: scrollController, + children: [ + _rowBanner(), + IntrinsicHeight(child: _responsiveRowInfo()), + const Divider(height: 1.0), + const VSpace(10), + RowDocument( + viewId: widget.rowController.viewId, + rowId: widget.rowController.rowId, + scrollController: scrollController, + ), + ], + ), + ), + ); + } + + Widget _rowBanner() { + return BlocBuilder( + builder: (context, state) { + final paddingOffset = getHorizontalPadding(context); + return Padding( + padding: EdgeInsets.only( + left: paddingOffset, + right: paddingOffset, + top: 20, + ), + child: RowBanner( + rowMeta: widget.rowController.rowMeta, + viewId: widget.rowController.viewId, + cellBuilder: (fieldId) { + final fieldInfo = state.cells + .firstWhereOrNull( + (e) => e.fieldInfo.field.id == fieldId, + ) + ?.fieldInfo; + + if (fieldInfo != null) { + final style = GridTextCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + textStyle: Theme.of(context).textTheme.titleLarge, + showEmoji: false, + autofocus: true, + ); + final cellContext = DatabaseCellContext( + viewId: widget.rowController.viewId, + rowMeta: widget.rowController.rowMeta, + fieldInfo: fieldInfo, + ); + return widget.cellBuilder.build(cellContext, style: style); + } else { + return const SizedBox.shrink(); + } + }, + ), + ); + }, + ); + } + + Widget _responsiveRowInfo() { + final rowDataColumn = RowPropertyList( + cellBuilder: widget.cellBuilder, + viewId: widget.rowController.viewId, + ); + final rowOptionColumn = RowActionList( + viewId: widget.rowController.viewId, + rowController: widget.rowController, + ); + final paddingOffset = getHorizontalPadding(context); + if (MediaQuery.of(context).size.width > 800) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 3, + child: Padding( + padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20), + child: rowDataColumn, + ), + ), + const VerticalDivider(width: 1.0), + Flexible( + child: Padding( + padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0), + child: rowOptionColumn, + ), + ), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20), + child: rowDataColumn, + ), + const Divider(height: 1.0), + Padding( + padding: EdgeInsets.symmetric(horizontal: paddingOffset), + child: rowOptionColumn, + ) + ], + ); + } + } +} + +double getHorizontalPadding(BuildContext context) { + if (MediaQuery.of(context).size.width > 800) { + return 50; + } else { + return 20; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart new file mode 100644 index 0000000000..20ad2bc402 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart @@ -0,0 +1,132 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart'; +import 'package:appflowy/plugins/document/application/doc_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +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'; + +class RowDocument extends StatelessWidget { + const RowDocument({ + super.key, + required this.viewId, + required this.rowId, + required this.scrollController, + }); + + final String viewId; + final String rowId; + final ScrollController scrollController; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RowDocumentBloc( + viewId: viewId, + rowId: rowId, + )..add( + const RowDocumentEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return state.loadingState.when( + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + error: (error) => FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + finish: () => RowEditor( + viewPB: state.viewPB!, + scrollController: scrollController, + ), + ); + }, + ), + ); + } +} + +class RowEditor extends StatefulWidget { + const RowEditor({ + super.key, + required this.viewPB, + required this.scrollController, + }); + + final ViewPB viewPB; + final ScrollController scrollController; + + @override + State createState() => _RowEditorState(); +} + +class _RowEditorState extends State { + late final DocumentBloc documentBloc; + + @override + void initState() { + super.initState(); + documentBloc = DocumentBloc(view: widget.viewPB) + ..add(const DocumentEvent.initial()); + } + + @override + dispose() { + documentBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => DocumentAppearanceCubit()), + BlocProvider.value(value: documentBloc), + ], + child: BlocBuilder( + builder: (context, state) { + return state.loadingState.when( + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + finish: (result) { + return result.fold( + (error) => FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + (_) { + final editorState = documentBloc.editorState; + if (editorState == null) { + return const SizedBox.shrink(); + } + return IntrinsicHeight( + child: Container( + constraints: const BoxConstraints(minHeight: 300), + child: AppFlowyEditorPage( + shrinkWrap: true, + autoFocus: false, + editorState: editorState, + scrollController: widget.scrollController, + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.symmetric(horizontal: 10), + ), + ), + ), + ); + }, + ); + }, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart new file mode 100644 index 0000000000..19bb71f064 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -0,0 +1,192 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_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'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'accessory/cell_accessory.dart'; +import 'cell_builder.dart'; +import 'cells/date_cell/date_cell.dart'; +import 'cells/select_option_cell/select_option_cell.dart'; +import 'cells/text_cell/text_cell.dart'; +import 'cells/url_cell/url_cell.dart'; + +/// Display the row properties in a list. Only use this widget in the +/// [RowDetailPage]. +/// +class RowPropertyList extends StatelessWidget { + final String viewId; + final GridCellBuilder cellBuilder; + const RowPropertyList({ + required this.viewId, + required this.cellBuilder, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.cells != current.cells, + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // The rest of the fields are displayed in the order of the field + // list + ...state.cells + .where((element) => !element.fieldInfo.isPrimary) + .map( + (cell) => _PropertyCell( + cellContext: cell, + cellBuilder: cellBuilder, + ), + ) + .toList(), + const VSpace(20), + + // Create a new property(field) button + CreateRowFieldButton(viewId: viewId), + ], + ); + }, + ); + } +} + +class _PropertyCell extends StatefulWidget { + final DatabaseCellContext cellContext; + final GridCellBuilder cellBuilder; + const _PropertyCell({ + required this.cellContext, + required this.cellBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _PropertyCellState(); +} + +class _PropertyCellState extends State<_PropertyCell> { + final PopoverController popover = PopoverController(); + + @override + Widget build(BuildContext context) { + final style = _customCellStyle(widget.cellContext.fieldType); + final cell = widget.cellBuilder.build(widget.cellContext, style: style); + + final gesture = GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => cell.beginFocus.notify(), + child: AccessoryHover( + contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3), + child: cell, + ), + ); + + return IntrinsicHeight( + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppFlowyPopover( + controller: popover, + constraints: BoxConstraints.loose(const Size(240, 600)), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + popupBuilder: (popoverContext) => buildFieldEditor(), + child: SizedBox( + width: 150, + child: FieldCellButton( + field: widget.cellContext.fieldInfo.field, + onTap: () => popover.show(), + radius: BorderRadius.circular(6), + ), + ), + ), + Expanded(child: gesture), + ], + ), + ), + ); + } + + Widget buildFieldEditor() { + return FieldEditor( + viewId: widget.cellContext.viewId, + isGroupingField: widget.cellContext.fieldInfo.isGroupField, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.cellContext.viewId, + field: widget.cellContext.fieldInfo.field, + ), + onHidden: (fieldId) { + popover.close(); + context.read().add(RowDetailEvent.hideField(fieldId)); + }, + onDeleted: (fieldId) { + popover.close(); + + NavigatorAlertDialog( + title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + confirm: () { + context + .read() + .add(RowDetailEvent.deleteField(fieldId)); + }, + ).show(context); + }, + ); + } +} + +GridCellStyle? _customCellStyle(FieldType fieldType) { + switch (fieldType) { + case FieldType.Checkbox: + return null; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateCellStyle( + alignment: Alignment.centerLeft, + ); + case FieldType.MultiSelect: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.Checklist: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.Number: + return null; + case FieldType.RichText: + return GridTextCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.SingleSelect: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + + case FieldType.URL: + return GridURLCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + accessoryTypes: [ + GridURLCellAccessoryType.copyURL, + GridURLCellAccessoryType.visitURL, + ], + ); + } + throw UnimplementedError; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart new file mode 100644 index 0000000000..ea51d1f675 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart @@ -0,0 +1,75 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; + +import '../../grid/presentation/layout/sizes.dart'; +import 'setting_button.dart'; + +class DatabaseSettingList extends StatelessWidget { + final DatabaseController databaseContoller; + final Function(DatabaseSettingAction, DatabaseController) onAction; + const DatabaseSettingList({ + required this.databaseContoller, + required this.onAction, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout) + .map((action) { + return DatabaseSettingItem( + action: action, + onAction: (action) => onAction(action, databaseContoller), + ); + }).toList(); + + return ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ); + } +} + +class DatabaseSettingItem extends StatelessWidget { + final DatabaseSettingAction action; + final Function(DatabaseSettingAction) onAction; + + const DatabaseSettingItem({ + required this.action, + required this.onAction, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + action.title(), + color: AFThemeExtension.of(context).textColor, + ), + onTap: () => onAction(action), + leftIcon: svgWidget( + action.iconName(), + color: Theme.of(context).iconTheme.color, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart new file mode 100644 index 0000000000..db6ce38f28 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart @@ -0,0 +1,202 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart'; +import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.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/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:styled_widget/styled_widget.dart'; + +import '../../grid/presentation/layout/sizes.dart'; +import '../../grid/presentation/widgets/toolbar/grid_layout.dart'; +import '../field/grid_property.dart'; +import 'database_setting.dart'; + +class SettingButton extends StatefulWidget { + final DatabaseController databaseController; + const SettingButton({ + required this.databaseController, + Key? key, + }) : super(key: key); + + @override + State createState() => _SettingButtonState(); +} + +class _SettingButtonState extends State { + late PopoverController _popoverController; + + @override + void initState() { + _popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + child: AppFlowyPopover( + controller: _popoverController, + constraints: BoxConstraints.loose(const Size(200, 400)), + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 8), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + child: FlowyTextButton( + LocaleKeys.settings_title.tr(), + fontColor: AFThemeExtension.of(context).textColor, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.typeOptionContentInsets, + onPressed: () => _popoverController.show(), + ), + popupBuilder: (BuildContext context) { + return _DatabaseSettingListPopover( + databaseController: widget.databaseController, + ); + }, + ), + ); + } +} + +class _DatabaseSettingListPopover extends StatefulWidget { + final DatabaseController databaseController; + + const _DatabaseSettingListPopover({ + required this.databaseController, + Key? key, + }) : super(key: key); + + @override + State createState() => _DatabaseSettingListPopoverState(); +} + +class _DatabaseSettingListPopoverState + extends State<_DatabaseSettingListPopover> { + DatabaseSettingAction? _action; + + @override + Widget build(BuildContext context) { + if (_action == null) { + return DatabaseSettingList( + databaseContoller: widget.databaseController, + onAction: (action, settingContext) { + setState(() { + _action = action; + }); + }, + ).padding(all: 6.0); + } else { + switch (_action!) { + case DatabaseSettingAction.showLayout: + return DatabaseLayoutList( + viewId: widget.databaseController.viewId, + currentLayout: widget.databaseController.databaseLayout, + ); + case DatabaseSettingAction.showGroup: + return DatabaseGroupList( + viewId: widget.databaseController.viewId, + fieldController: widget.databaseController.fieldController, + onDismissed: () { + // widget.popoverController.close(); + }, + ); + case DatabaseSettingAction.showProperties: + return DatabasePropertyList( + viewId: widget.databaseController.viewId, + fieldController: widget.databaseController.fieldController, + ); + case DatabaseSettingAction.showCalendarLayout: + return CalendarLayoutSetting( + viewId: widget.databaseController.viewId, + fieldController: widget.databaseController.fieldController, + calendarSettingController: ICalendarSettingImpl( + widget.databaseController, + ), + ); + } + } + } +} + +class ICalendarSettingImpl extends ICalendarSetting { + final DatabaseController _databaseController; + + ICalendarSettingImpl(this._databaseController); + + @override + void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) { + _databaseController.updateLayoutSetting(layoutSettings); + } + + @override + CalendarLayoutSettingPB? getLayoutSetting() { + return _databaseController.databaseLayoutSetting?.calendar; + } +} + +enum DatabaseSettingAction { + showProperties, + showLayout, + showGroup, + showCalendarLayout, +} + +extension DatabaseSettingActionExtension on DatabaseSettingAction { + String iconName() { + switch (this) { + case DatabaseSettingAction.showProperties: + return 'grid/setting/properties'; + case DatabaseSettingAction.showLayout: + return 'grid/setting/database_layout'; + case DatabaseSettingAction.showGroup: + return 'grid/setting/group'; + case DatabaseSettingAction.showCalendarLayout: + return 'grid/setting/calendar_layout'; + } + } + + String title() { + switch (this) { + case DatabaseSettingAction.showProperties: + return LocaleKeys.grid_settings_Properties.tr(); + case DatabaseSettingAction.showLayout: + return LocaleKeys.grid_settings_databaseLayout.tr(); + case DatabaseSettingAction.showGroup: + return LocaleKeys.grid_settings_group.tr(); + case DatabaseSettingAction.showCalendarLayout: + return LocaleKeys.calendar_settings_name.tr(); + } + } +} + +/// Returns the list of actions that should be shown for the given database layout. +List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { + switch (layout) { + case DatabaseLayoutPB.Board: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + DatabaseSettingAction.showGroup, + ]; + case DatabaseLayoutPB.Calendar: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + DatabaseSettingAction.showCalendarLayout, + ]; + case DatabaseLayoutPB.Grid: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + ]; + default: + return []; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart new file mode 100644 index 0000000000..98498781ef --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -0,0 +1,203 @@ +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; +import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; +import 'package:appflowy/plugins/trash/application/trash_service.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/util/json_print.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/application/doc/doc_listener.dart'; +import 'package:appflowy/plugins/document/application/doc_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + show EditorState, LogLevel, TransactionTime; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; +import 'dart:async'; +part 'doc_bloc.freezed.dart'; + +class DocumentBloc extends Bloc { + DocumentBloc({ + required this.view, + }) : _documentListener = DocumentListener(id: view.id), + _viewListener = ViewListener(viewId: view.id), + _documentService = DocumentService(), + _trashService = TrashService(), + super(DocumentState.initial()) { + _transactionAdapter = TransactionAdapter( + documentId: view.id, + documentService: _documentService, + ); + on(_onDocumentEvent); + } + + final ViewPB view; + + final DocumentListener _documentListener; + final ViewListener _viewListener; + + final DocumentService _documentService; + final TrashService _trashService; + + late final TransactionAdapter _transactionAdapter; + + EditorState? editorState; + StreamSubscription? _subscription; + + @override + Future close() async { + await _viewListener.stop(); + await _subscription?.cancel(); + await _documentService.closeDocument(view: view); + editorState?.cancelSubscription(); + return super.close(); + } + + Future _onDocumentEvent( + DocumentEvent event, + Emitter emit, + ) async { + await event.map( + initial: (Initial value) async { + final state = await _fetchDocumentState(); + await _subscribe(state); + emit(state); + }, + moveToTrash: (MoveToTrash value) async { + emit(state.copyWith(isDeleted: true)); + }, + restore: (Restore value) async { + emit(state.copyWith(isDeleted: false)); + }, + deletePermanently: (DeletePermanently value) async { + final result = await _trashService.deleteViews([view.id]); + final forceClose = result.fold((l) => true, (r) => false); + emit(state.copyWith(forceClose: forceClose)); + }, + restorePage: (RestorePage value) async { + final result = await _trashService.putback(view.id); + final isDeleted = result.fold((l) => false, (r) => true); + emit(state.copyWith(isDeleted: isDeleted)); + }, + ); + } + + Future _subscribe(DocumentState state) async { + _onViewChanged(); + _onDocumentChanged(); + + // create the editor state + await state.loadingState.whenOrNull( + finish: (data) async => data.map((r) { + _initAppFlowyEditorState(r); + }), + ); + } + + /// subscribe to the view(document page) change + void _onViewChanged() { + _viewListener.start( + onViewMoveToTrash: (r) { + r.swap().map((r) => add(const DocumentEvent.moveToTrash())); + }, + onViewDeleted: (r) { + r.swap().map((r) => add(const DocumentEvent.moveToTrash())); + }, + onViewRestored: (r) => + r.swap().map((r) => add(const DocumentEvent.restore())), + ); + } + + /// subscribe to the document content change + void _onDocumentChanged() { + _documentListener.start( + didReceiveUpdate: (docEvent) { + // todo: integrate the document change to the editor + // prettyPrintJson(docEvent.toProto3Json()); + }, + ); + } + + /// Fetch document + Future _fetchDocumentState() async { + final result = await UserBackendService.getCurrentUserProfile().then( + (value) async => value.andThen( + // open the document + await _documentService.openDocument(view: view), + ), + ); + return state.copyWith( + loadingState: DocumentLoadingState.finish(result), + ); + } + + Future _initAppFlowyEditorState(DocumentDataPB data) async { + if (kDebugMode) { + prettyPrintJson(data.toProto3Json()); + } + + final document = data.toDocument(); + if (document == null) { + assert(false, 'document is null'); + return; + } + + final editorState = EditorState(document: document); + this.editorState = editorState; + + // subscribe to the document change from the editor + _subscription = editorState.transactionStream.listen((event) async { + final time = event.$1; + if (time != TransactionTime.before) { + return; + } + await _transactionAdapter.apply(event.$2, editorState); + }); + + // output the log from the editor when debug mode + if (kDebugMode) { + editorState.logConfiguration + ..level = LogLevel.all + ..handler = (log) { + // Log.debug(log); + }; + } + } +} + +@freezed +class DocumentEvent with _$DocumentEvent { + const factory DocumentEvent.initial() = Initial; + const factory DocumentEvent.moveToTrash() = MoveToTrash; + const factory DocumentEvent.restore() = Restore; + const factory DocumentEvent.restorePage() = RestorePage; + const factory DocumentEvent.deletePermanently() = DeletePermanently; +} + +@freezed +class DocumentState with _$DocumentState { + const factory DocumentState({ + required DocumentLoadingState loadingState, + required bool isDeleted, + required bool forceClose, + UserProfilePB? userProfilePB, + }) = _DocumentState; + + factory DocumentState.initial() => const DocumentState( + loadingState: _Loading(), + isDeleted: false, + forceClose: false, + userProfilePB: null, + ); +} + +@freezed +class DocumentLoadingState with _$DocumentLoadingState { + const factory DocumentLoadingState.loading() = _Loading; + const factory DocumentLoadingState.finish( + Either successOrFail, + ) = _Finish; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart new file mode 100644 index 0000000000..93bea86ba1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart @@ -0,0 +1,49 @@ +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; + +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart'; + +class DocumentService { + // unused now. + Future> createDocument({ + required ViewPB view, + }) async { + final canOpen = await openDocument(view: view); + if (canOpen.isRight()) { + return const Right(unit); + } + final payload = CreateDocumentPayloadPB()..documentId = view.id; + final result = await DocumentEventCreateDocument(payload).send(); + return result.swap(); + } + + Future> openDocument({ + required ViewPB view, + }) async { + final payload = OpenDocumentPayloadPB()..documentId = view.id; + final result = await DocumentEventOpenDocument(payload).send(); + return result.swap(); + } + + Future> closeDocument({ + required ViewPB view, + }) async { + final payload = CloseDocumentPayloadPB()..documentId = view.id; + final result = await DocumentEventCloseDocument(payload).send(); + return result.swap(); + } + + Future> applyAction({ + required String documentId, + required Iterable actions, + }) async { + final payload = ApplyActionPayloadPB( + documentId: documentId, + actions: actions, + ); + final result = await DocumentEventApplyAction(payload).send(); + return result.swap(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_state_listener.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_state_listener.dart deleted file mode 100644 index 7f73147d79..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_state_listener.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/document_notification.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-notification/subject.pb.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -typedef DocumentSyncStateCallback = void Function( - DocumentSyncStatePB syncState, -); - -class DocumentSyncStateListener { - DocumentSyncStateListener({ - required this.id, - }); - - final String id; - StreamSubscription? _subscription; - DocumentNotificationParser? _parser; - DocumentSyncStateCallback? didReceiveSyncState; - - void start({ - DocumentSyncStateCallback? didReceiveSyncState, - }) { - this.didReceiveSyncState = didReceiveSyncState; - - _parser = DocumentNotificationParser( - id: id, - callback: _callback, - ); - _subscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - void _callback( - DocumentNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DocumentNotification.DidUpdateDocumentSyncState: - result.map( - (r) { - final value = DocumentSyncStatePB.fromBuffer(r); - didReceiveSyncState?.call(value); - }, - ); - break; - default: - break; - } - } - - Future stop() async { - await _subscription?.cancel(); - _subscription = null; - } -} 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 deleted file mode 100644 index c65d818351..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart +++ /dev/null @@ -1,217 +0,0 @@ -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: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, - }); - - final double fontSize; - final String fontFamily; - final String codeFontFamily; - 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`. - /// - /// This is necessary because simply passing `null` as the value does not distinguish between wanting to - /// set the field to `null` and not wanting to update the field at all. - DocumentAppearance copyWith({ - double? fontSize, - String? fontFamily, - String? codeFontFamily, - Color? cursorColor, - Color? selectionColor, - String? defaultTextDirection, - bool cursorColorIsNull = false, - bool selectionColorIsNull = false, - bool textDirectionIsNull = false, - double? width, - }) { - return DocumentAppearance( - fontSize: fontSize ?? this.fontSize, - fontFamily: fontFamily ?? this.fontFamily, - codeFontFamily: codeFontFamily ?? this.codeFontFamily, - cursorColor: cursorColorIsNull ? null : cursorColor ?? this.cursorColor, - selectionColor: - selectionColorIsNull ? null : selectionColor ?? this.selectionColor, - defaultTextDirection: textDirectionIsNull - ? null - : defaultTextDirection ?? this.defaultTextDirection, - width: width ?? this.width, - ); - } -} - -class DocumentAppearanceCubit extends Cubit { - DocumentAppearanceCubit() - : super( - DocumentAppearance( - fontSize: 16.0, - fontFamily: defaultFontFamily, - codeFontFamily: builtInCodeFontFamily, - width: UniversalPlatform.isMobile - ? double.infinity - : EditorStyleCustomizer.maxDocumentWidth, - ), - ); - - Future fetch() async { - final prefs = await SharedPreferences.getInstance(); - final fontSize = - prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0; - final fontFamily = prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ?? - defaultFontFamily; - final defaultTextDirection = - prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection); - - final cursorColorString = - prefs.getString(KVKeys.kDocumentAppearanceCursorColor); - final selectionColorString = - prefs.getString(KVKeys.kDocumentAppearanceSelectionColor); - final cursorColor = - cursorColorString != null ? Color(int.parse(cursorColorString)) : null; - 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'); - - if (isClosed) { - return; - } - - emit( - state.copyWith( - fontSize: fontSize * textScaleFactor, - fontFamily: fontFamily, - cursorColor: cursorColor, - selectionColor: selectionColor, - defaultTextDirection: defaultTextDirection, - cursorColorIsNull: cursorColor == null, - selectionColorIsNull: selectionColor == null, - textDirectionIsNull: defaultTextDirection == null, - width: width, - ), - ); - } - - Future syncFontSize(double fontSize) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setDouble(KVKeys.kDocumentAppearanceFontSize, fontSize); - - if (!isClosed) { - emit(state.copyWith(fontSize: fontSize)); - } - } - - Future syncFontFamily(String fontFamily) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(KVKeys.kDocumentAppearanceFontFamily, fontFamily); - - if (!isClosed) { - emit(state.copyWith(fontFamily: fontFamily)); - } - } - - Future syncDefaultTextDirection(String? direction) async { - final prefs = await SharedPreferences.getInstance(); - if (direction == null) { - await prefs.remove(KVKeys.kDocumentAppearanceDefaultTextDirection); - } else { - await prefs.setString( - KVKeys.kDocumentAppearanceDefaultTextDirection, - direction, - ); - } - - if (!isClosed) { - emit( - state.copyWith( - defaultTextDirection: direction, - textDirectionIsNull: direction == null, - ), - ); - } - } - - Future syncCursorColor(Color? cursorColor) async { - final prefs = await SharedPreferences.getInstance(); - - if (cursorColor == null) { - await prefs.remove(KVKeys.kDocumentAppearanceCursorColor); - } else { - await prefs.setString( - KVKeys.kDocumentAppearanceCursorColor, - cursorColor.toHexString(), - ); - } - - if (!isClosed) { - emit( - state.copyWith( - cursorColor: cursorColor, - cursorColorIsNull: cursorColor == null, - ), - ); - } - } - - Future syncSelectionColor(Color? selectionColor) async { - final prefs = await SharedPreferences.getInstance(); - - if (selectionColor == null) { - await prefs.remove(KVKeys.kDocumentAppearanceSelectionColor); - } else { - await prefs.setString( - KVKeys.kDocumentAppearanceSelectionColor, - selectionColor.toHexString(), - ); - } - - if (!isClosed) { - emit( - state.copyWith( - selectionColor: selectionColor, - selectionColorIsNull: selectionColor == null, - ), - ); - } - } - - 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_awareness_metadata.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_awareness_metadata.dart deleted file mode 100644 index 6bb19ef8bb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_awareness_metadata.dart +++ /dev/null @@ -1,22 +0,0 @@ -// This file is "main.dart" -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'document_awareness_metadata.freezed.dart'; -part 'document_awareness_metadata.g.dart'; - -@freezed -class DocumentAwarenessMetadata with _$DocumentAwarenessMetadata { - const factory DocumentAwarenessMetadata({ - // ignore: invalid_annotation_target - @JsonKey(name: 'cursor_color') required String cursorColor, - // ignore: invalid_annotation_target - @JsonKey(name: 'selection_color') required String selectionColor, - // ignore: invalid_annotation_target - @JsonKey(name: 'user_name') required String userName, - // ignore: invalid_annotation_target - @JsonKey(name: 'user_avatar') required String userAvatar, - }) = _DocumentAwarenessMetadata; - - factory DocumentAwarenessMetadata.fromJson(Map json) => - _$DocumentAwarenessMetadataFromJson(json); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart deleted file mode 100644 index 264ec4bb11..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ /dev/null @@ -1,487 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart'; -import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart'; -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/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'; -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_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; -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'; - -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), - _syncStateListener = DocumentSyncStateListener(id: documentId), - super(DocumentState.initial()) { - _viewListener = databaseViewId == null && rowId == null - ? ViewListener(viewId: documentId) - : null; - 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; - - final DocumentService _documentService = DocumentService(); - final TrashService _trashService = TrashService(); - - late DocumentCollabAdapter _documentCollabAdapter; - - late final TransactionAdapter _transactionAdapter = TransactionAdapter( - documentId: documentId, - documentService: _documentService, - ); - - late final DocumentRules _documentRules; - - StreamSubscription? _transactionSubscription; - - bool isClosing = false; - - static const _syncDuration = Duration(milliseconds: 250); - final _updateSelectionDebounce = Debounce(duration: _syncDuration); - final _syncThrottle = Throttler(duration: _syncDuration); - - // The conflict handle logic is not fully implemented yet - // use the syncTimer to force to reload the document state when the conflict happens. - Timer? _syncTimer; - - bool get isLocalMode { - final userProfilePB = state.userProfilePB; - final type = userProfilePB?.workspaceAuthType ?? AuthTypePB.Local; - return type == AuthTypePB.Local; - } - - @override - Future close() async { - isClosing = true; - if (_saveToBlocMap) { - _documentBlocMap.remove(documentId); - } - await checkDocumentIntegrity(); - await _cancelSubscriptions(); - _clearEditorState(); - return super.close(); - } - - Future _cancelSubscriptions() async { - 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(); - } - - Future _onDocumentEvent( - DocumentEvent event, - Emitter emit, - ) async { - await event.when( - initial: () async { - if (_saveToBlocMap) { - _documentBlocMap[documentId] = this; - } - final result = await _fetchDocumentState(); - _onViewChanged(); - _onDocumentChanged(); - final newState = await result.fold( - (s) async { - final userProfilePB = - await getIt().getUser().toNullable(); - return state.copyWith( - error: null, - editorState: s, - isLoading: false, - userProfilePB: userProfilePB, - ); - }, - (f) async => state.copyWith( - error: f, - editorState: null, - isLoading: false, - ), - ); - emit(newState); - if (newState.userProfilePB != null) { - await _updateCollaborator(); - } - }, - moveToTrash: () async { - emit(state.copyWith(isDeleted: true)); - }, - restore: () async { - emit(state.copyWith(isDeleted: false)); - }, - deletePermanently: () async { - if (databaseViewId == null && rowId == null) { - final result = await _trashService.deleteViews([documentId]); - final forceClose = result.fold((l) => true, (r) => false); - emit(state.copyWith(forceClose: forceClose)); - } - }, - restorePage: () async { - if (databaseViewId == null && rowId == null) { - final result = await TrashService.putback(documentId); - final isDeleted = result.fold((l) => false, (r) => true); - emit(state.copyWith(isDeleted: isDeleted)); - } - }, - syncStateChanged: (syncState) { - emit(state.copyWith(syncState: syncState.value)); - }, - clearAwarenessStates: () async { - // sync a null selection and a null meta to clear the awareness states - await _documentService.syncAwarenessStates( - documentId: documentId, - ); - }, - syncAwarenessStates: () async { - await _updateCollaborator(); - }, - ); - } - - /// subscribe to the view(document page) change - void _onViewChanged() { - _viewListener?.start( - onViewMoveToTrash: (r) { - r.map((r) => add(const DocumentEvent.moveToTrash())); - }, - onViewDeleted: (r) { - r.map((r) => add(const DocumentEvent.moveToTrash())); - }, - onViewRestored: (r) => r.map((r) => add(const DocumentEvent.restore())), - ); - } - - /// subscribe to the document content change - void _onDocumentChanged() { - _documentListener.start( - onDocEventUpdate: _throttleSyncDoc, - onDocAwarenessUpdate: _onAwarenessStatesUpdate, - ); - - _syncStateListener.start( - didReceiveSyncState: (syncState) { - if (!isClosed) { - add(DocumentEvent.syncStateChanged(syncState)); - } - }, - ); - } - - /// Fetch document - Future> _fetchDocumentState() async { - final result = await _documentService.openDocument(documentId: documentId); - return result.fold( - (s) async => FlowyResult.success(await _initAppFlowyEditorState(s)), - (e) => FlowyResult.failure(e), - ); - } - - 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'); - return null; - } - - 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; - 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}', - ); - } - - if (!isClosed) { - // ignore: invalid_use_of_visible_for_testing_member - emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty)); - } - }, - ); - - editorState.selectionNotifier.addListener(_debounceOnSelectionUpdate); - - // output the log from the editor when debug mode - if (kDebugMode) { - editorState.logConfiguration - ..level = AppFlowyEditorLogLevel.all - ..handler = (log) { - if (enableDocumentInternalLog) { - // Log.info(log); - } - }; - } - - return editorState; - } - - Future _onDocumentStateUpdate(DocEventPB docEvent) async { - if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) { - return; - } - - unawaited(_documentCollabAdapter.syncV3(docEvent: docEvent)); - } - - Future _onAwarenessStatesUpdate( - DocumentAwarenessStatesPB awarenessStates, - ) async { - if (!FeatureFlag.syncDocument.isOn) { - return; - } - - final userId = state.userProfilePB?.id; - if (userId != null) { - await _documentCollabAdapter.updateRemoteSelection( - userId.toString(), - awarenessStates, - ); - } - } - - void _debounceOnSelectionUpdate() { - _updateSelectionDebounce.call(_onSelectionUpdate); - } - - void _throttleSyncDoc(DocEventPB docEvent) { - if (enableDocumentInternalLog) { - Log.info('[DocumentBloc] throttle sync doc: ${docEvent.toProto3Json()}'); - } - _syncThrottle.call(() { - _onDocumentStateUpdate(docEvent); - }); - } - - Future _onSelectionUpdate() async { - if (isClosing) { - return; - } - final user = state.userProfilePB; - final deviceId = ApplicationInfo.deviceId; - if (!FeatureFlag.syncDocument.isOn || user == null) { - return; - } - - final editorState = state.editorState; - if (editorState == null) { - return; - } - final selection = editorState.selection; - - // sync the selection - final id = user.id.toString() + deviceId; - final basicColor = ColorGenerator(id.toString()).toColor(); - final metadata = DocumentAwarenessMetadata( - cursorColor: basicColor.toHexString(), - selectionColor: basicColor.withValues(alpha: 0.6).toHexString(), - userName: user.name, - userAvatar: user.iconUrl, - ); - await _documentService.syncAwarenessStates( - documentId: documentId, - selection: selection, - metadata: jsonEncode(metadata.toJson()), - ); - } - - Future _updateCollaborator() async { - final user = state.userProfilePB; - final deviceId = ApplicationInfo.deviceId; - if (!FeatureFlag.syncDocument.isOn || user == null) { - return; - } - - // sync the selection - final id = user.id.toString() + deviceId; - final basicColor = ColorGenerator(id.toString()).toColor(); - final metadata = DocumentAwarenessMetadata( - cursorColor: basicColor.toHexString(), - selectionColor: basicColor.withValues(alpha: 0.6).toHexString(), - userName: user.name, - userAvatar: user.iconUrl, - ); - await _documentService.syncAwarenessStates( - documentId: documentId, - metadata: jsonEncode(metadata.toJson()), - ); - } - - 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, - ); - } - } - } -} - -@freezed -class DocumentEvent with _$DocumentEvent { - const factory DocumentEvent.initial() = Initial; - const factory DocumentEvent.moveToTrash() = MoveToTrash; - const factory DocumentEvent.restore() = Restore; - const factory DocumentEvent.restorePage() = RestorePage; - const factory DocumentEvent.deletePermanently() = DeletePermanently; - const factory DocumentEvent.syncStateChanged( - final DocumentSyncStatePB syncState, - ) = syncStateChanged; - const factory DocumentEvent.syncAwarenessStates() = SyncAwarenessStates; - const factory DocumentEvent.clearAwarenessStates() = ClearAwarenessStates; -} - -@freezed -class DocumentState with _$DocumentState { - const factory DocumentState({ - required final bool isDeleted, - required final bool forceClose, - required final bool isLoading, - required final DocumentSyncState syncState, - bool? isDocumentEmpty, - UserProfilePB? userProfilePB, - EditorState? editorState, - FlowyError? error, - @Default(null) DocumentAwarenessStatesPB? awarenessStates, - }) = _DocumentState; - - factory DocumentState.initial() => const DocumentState( - isDeleted: false, - forceClose: false, - isLoading: true, - syncState: DocumentSyncState.Syncing, - ); -} 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 deleted file mode 100644 index f550093b54..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart +++ /dev/null @@ -1,273 +0,0 @@ -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'; -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: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, - ); - - final EditorState editorState; - final String docId; - final DocumentDiff diff = const DocumentDiff(); - - final _service = DocumentService(); - - /// Sync version 1 - /// - /// Force to reload the document - /// - /// Only use in development - Future syncV1() async { - final result = await _service.getDocument(documentId: docId); - final document = result.fold((s) => s.toDocument(), (f) => null); - if (document == null) { - return null; - } - return EditorState(document: document); - } - - /// Sync version 2 - /// - /// Translate the [docEvent] from yrs to [Operation]s and apply it to the [editorState] - /// - /// Not fully implemented yet - Future syncV2(DocEventPB docEvent) async { - prettyPrintJson(docEvent.toProto3Json()); - - final transaction = editorState.transaction; - - for (final event in docEvent.events) { - for (final blockEvent in event.event) { - switch (blockEvent.command) { - case DeltaTypePB.Inserted: - break; - case DeltaTypePB.Updated: - await _syncUpdated(blockEvent, transaction); - break; - case DeltaTypePB.Removed: - break; - default: - } - } - } - - await editorState.apply(transaction, isRemote: true); - } - - /// Sync version 3 - /// - /// Diff the local document with the remote document and apply the changes - Future syncV3({DocEventPB? docEvent}) async { - final result = await _service.getDocument(documentId: docId); - final document = result.fold((s) => s.toDocument(), (f) => null); - if (document == null) { - return; - } - - final ops = diff.diffDocument(editorState.document, document); - if (ops.isEmpty) { - return; - } - - if (enableDocumentInternalLog) { - prettyPrintJson(ops.map((op) => op.toJson()).toList()); - } - - final transaction = editorState.transaction; - for (final op in ops) { - transaction.add(op); - } - 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; - }()); - } - } - - Future forceReload() async { - final result = await _service.getDocument(documentId: docId); - final document = result.fold((s) => s.toDocument(), (f) => null); - if (document == null) { - return; - } - - final beforeSelection = editorState.selection; - - final clear = editorState.transaction; - clear.deleteNodes(editorState.document.root.children); - await editorState.apply(clear, isRemote: true); - - final insert = editorState.transaction; - insert.insertNodes([0], document.root.children); - await editorState.apply(insert, isRemote: true); - - editorState.selection = beforeSelection; - } - - Future _syncUpdated( - BlockEventPayloadPB payload, - Transaction transaction, - ) async { - assert(payload.command == DeltaTypePB.Updated); - - final path = payload.path; - final id = payload.id; - final value = jsonDecode(payload.value); - - final nodes = NodeIterator( - document: editorState.document, - startNode: editorState.document.root, - ).toList(); - - // 1. meta -> text_map = text delta change - if (path.isTextDeltaChangeset) { - // find the 'text' block and apply the delta - // ⚠️ not completed yet. - final target = nodes.singleWhereOrNull((n) => n.id == id); - if (target != null) { - try { - final delta = Delta.fromJson(jsonDecode(value)); - transaction.insertTextDelta(target, 0, delta); - } catch (e) { - Log.error('Failed to apply delta: $value, error: $e'); - } - } - } else if (path.isBlockChangeset) { - final target = nodes.singleWhereOrNull((n) => n.id == id); - if (target != null) { - try { - final delta = jsonDecode(value['data'])['delta']; - transaction.updateNode(target, { - 'delta': Delta.fromJson(delta).toJson(), - }); - } catch (e) { - Log.error('Failed to update $value, error: $e'); - } - } - } - } - - Future updateRemoteSelection( - String userId, - DocumentAwarenessStatesPB states, - ) async { - final List remoteSelections = []; - final deviceId = ApplicationInfo.deviceId; - // the values may be duplicated, sort by the timestamp and then filter the duplicated values - final values = states.value.values - .sorted( - (a, b) => b.timestamp.compareTo(a.timestamp), - ) // in descending order - .unique( - (e) => Object.hashAll([e.user.uid, e.user.deviceId]), - ); - for (final state in values) { - // the following code is only for version 1 - if (state.version != 1 || state.metadata.isEmpty) { - continue; - } - final uid = state.user.uid.toString(); - final did = state.user.deviceId; - - DocumentAwarenessMetadata metadata; - try { - metadata = DocumentAwarenessMetadata.fromJson( - jsonDecode(state.metadata), - ); - } catch (e) { - Log.error('Failed to parse metadata: $e, ${state.metadata}'); - continue; - } - final selectionColor = metadata.selectionColor.tryToColor(); - final cursorColor = metadata.cursorColor.tryToColor(); - if ((uid == userId && did == deviceId) || - (cursorColor == null || selectionColor == null)) { - continue; - } - final start = state.selection.start; - final end = state.selection.end; - final selection = Selection( - start: Position( - path: start.path.toIntList(), - offset: start.offset.toInt(), - ), - end: Position( - path: end.path.toIntList(), - offset: end.offset.toInt(), - ), - ); - final color = ColorGenerator(uid + did).toColor(); - final remoteSelection = RemoteSelection( - id: uid, - selection: selection, - selectionColor: selectionColor, - cursorColor: cursorColor, - builder: (_, __, rect) { - return Positioned( - top: rect.top - 14, - left: selection.isCollapsed ? rect.right : rect.left, - child: ColoredBox( - color: color, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 2.0, - vertical: 1.0, - ), - child: FlowyText( - metadata.userName, - color: Colors.black, - fontSize: 12.0, - ), - ), - ), - ); - }, - ); - remoteSelections.add(remoteSelection); - } - - editorState.remoteSelections.value = remoteSelections; - } -} - -extension on List { - List toIntList() { - return map((e) => e.toInt()).toList(); - } -} - -extension on List { - bool get isTextDeltaChangeset { - return length == 3 && this[0] == 'meta' && this[1] == 'text_map'; - } - - bool get isBlockChangeset { - return length == 2 && this[0] == 'blocks'; - } -} 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 deleted file mode 100644 index a0678372cf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart'; -import 'package:appflowy/plugins/document/application/document_listener.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/device_info_task.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'document_collaborators_bloc.freezed.dart'; - -bool _filterCurrentUser = false; - -class DocumentCollaboratorsBloc - extends Bloc { - DocumentCollaboratorsBloc({ - required this.view, - }) : _listener = DocumentListener(id: view.id), - super(DocumentCollaboratorsState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - final result = await getIt().getUser(); - final userProfile = result.fold((s) => s, (f) => null); - emit( - state.copyWith( - shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server, - ), - ); - final deviceId = ApplicationInfo.deviceId; - if (userProfile != null) { - _listener.start( - onDocAwarenessUpdate: (states) { - if (isClosed) { - return; - } - - add( - DocumentCollaboratorsEvent.update( - userProfile, - deviceId, - states, - ), - ); - }, - ); - } - }, - update: (userProfile, deviceId, states) { - final collaborators = _buildCollaborators( - userProfile, - deviceId, - states, - ); - emit(state.copyWith(collaborators: collaborators)); - }, - ); - }, - ); - } - - final ViewPB view; - final DocumentListener _listener; - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } - - List _buildCollaborators( - UserProfilePB userProfile, - String deviceId, - DocumentAwarenessStatesPB states, - ) { - final result = []; - 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); - for (final state in sorted) { - if (state.version != 1) { - continue; - } - // filter current user - if (_filterCurrentUser && - userProfile.id == state.user.uid && - deviceId == state.user.deviceId) { - continue; - } - try { - final metadata = DocumentAwarenessMetadata.fromJson( - jsonDecode(state.metadata), - ); - result.add(metadata); - } catch (e) { - Log.error('Failed to parse metadata: $e'); - } - } - return result; - } -} - -@freezed -class DocumentCollaboratorsEvent with _$DocumentCollaboratorsEvent { - const factory DocumentCollaboratorsEvent.initial() = Initial; - const factory DocumentCollaboratorsEvent.update( - UserProfilePB userProfile, - String deviceId, - DocumentAwarenessStatesPB states, - ) = Update; -} - -@freezed -class DocumentCollaboratorsState with _$DocumentCollaboratorsState { - const factory DocumentCollaboratorsState({ - @Default([]) List collaborators, - @Default(false) bool shouldShowIndicator, - }) = _DocumentCollaboratorsState; - - factory DocumentCollaboratorsState.initial() => - const DocumentCollaboratorsState(); -} 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..569d81fd69 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 @@ -1,34 +1,12 @@ import 'dart:convert'; -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_backend/protobuf/flowy-document2/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' - show - Document, - Node, - Attributes, - Delta, - ParagraphBlockKeys, - NodeIterator, - NodeExternalValues, - HeadingBlockKeys, - NumberedListBlockKeys, - BulletedListBlockKeys, - blockComponentDelta; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; + show Document, Node, Attributes, Delta, ParagraphBlockKeys, NodeIterator; +import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; -class ExternalValues extends NodeExternalValues { - const ExternalValues({ - required this.externalId, - required this.externalType, - }); - - final String externalId; - final String externalType; -} - extension DocumentDataPBFromTo on DocumentDataPB { static DocumentDataPB? fromDocument(Document document) { final startNode = document.first; @@ -64,11 +42,11 @@ extension DocumentDataPBFromTo on DocumentDataPB { // generate the meta final childrenMap = {}; - blocks.values.where((e) => e.parentId.isNotEmpty).forEach((value) { - final childrenId = blocks[value.parentId]?.childrenId; - if (childrenId != null) { - childrenMap[childrenId] ??= ChildrenPB.create(); - childrenMap[childrenId]!.children.add(value.id); + blocks.forEach((key, value) { + final parentId = value.parentId; + if (parentId.isNotEmpty) { + childrenMap[parentId] ??= ChildrenPB.create(); + childrenMap[parentId]!.children.add(value.id); } }); final meta = MetaPB(childrenMap: childrenMap); @@ -103,71 +81,33 @@ 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( - children: children, - meta: meta, - ); - - for (final element in children) { - element.parent = node; - } - - return node; + return block?.toNode(children: children); } } extension BlockToNode on BlockPB { Node toNode({ Iterable? children, - required MetaPB meta, }) { - final node = Node( + return Node( id: id, type: ty, - attributes: _dataAdapter(ty, data, meta), + attributes: _dataAdapter(ty, data), children: children ?? [], ); - node.externalValues = ExternalValues( - externalId: externalId, - externalType: externalType, - ); - return node; } - Attributes _dataAdapter(String ty, String data, MetaPB meta) { + Attributes _dataAdapter(String ty, String data) { final map = Attributes.from(jsonDecode(data)); - - // it used in the delta case now. - final externalType = this.externalType; - final externalId = this.externalId; - if (externalType.isNotEmpty && externalId.isNotEmpty) { - // the 'text' type is the only type that is supported now. - if (externalType == 'text') { - final deltaString = meta.textMap[externalId]; - if (deltaString != null) { - final delta = jsonDecode(deltaString); - map[blockComponentDelta] = delta; - } - } - } - - Attributes adapterCallback(Attributes map) => map - ..putIfAbsent( - blockComponentDelta, - () => Delta().toJson(), - ); - final adapter = { - ParagraphBlockKeys.type: adapterCallback, - HeadingBlockKeys.type: adapterCallback, - CodeBlockKeys.type: adapterCallback, - QuoteBlockKeys.type: adapterCallback, - NumberedListBlockKeys.type: adapterCallback, - BulletedListBlockKeys.type: adapterCallback, - ToggleListBlockKeys.type: adapterCallback, + ParagraphBlockKeys.type: (Attributes map) => map + ..putIfAbsent( + 'delta', + () => Delta().toJson(), + ), }; return adapter[ty]?.call(map) ?? map; } @@ -178,8 +118,6 @@ extension NodeToBlock on Node { String? parentId, String? childrenId, Attributes? attributes, - String? externalId, - String? externalType, }) { assert(id.isNotEmpty); final block = BlockPB.create() @@ -192,29 +130,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_listener.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_listener.dart deleted file mode 100644 index ab102c7ee8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_listener.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/document_notification.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-notification/subject.pb.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -typedef OnDocumentEventUpdate = void Function(DocEventPB docEvent); -typedef OnDocumentAwarenessStateUpdate = void Function( - DocumentAwarenessStatesPB awarenessStates, -); - -class DocumentListener { - DocumentListener({ - required this.id, - }); - - final String id; - - StreamSubscription? _subscription; - DocumentNotificationParser? _parser; - - OnDocumentEventUpdate? _onDocEventUpdate; - OnDocumentAwarenessStateUpdate? _onDocAwarenessUpdate; - - void start({ - OnDocumentEventUpdate? onDocEventUpdate, - OnDocumentAwarenessStateUpdate? onDocAwarenessUpdate, - }) { - _onDocEventUpdate = onDocEventUpdate; - _onDocAwarenessUpdate = onDocAwarenessUpdate; - - _parser = DocumentNotificationParser( - id: id, - callback: _callback, - ); - _subscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - void _callback( - DocumentNotification ty, - FlowyResult result, - ) { - switch (ty) { - case DocumentNotification.DidReceiveUpdate: - result.map( - (s) => _onDocEventUpdate?.call(DocEventPB.fromBuffer(s)), - ); - break; - case DocumentNotification.DidUpdateDocumentAwarenessState: - result.map( - (s) => _onDocAwarenessUpdate?.call( - DocumentAwarenessStatesPB.fromBuffer(s), - ), - ); - break; - default: - break; - } - } - - Future stop() async { - _onDocAwarenessUpdate = null; - _onDocEventUpdate = null; - await _subscription?.cancel(); - _subscription = null; - } -} 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 deleted file mode 100644 index f520e20d02..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart +++ /dev/null @@ -1,222 +0,0 @@ -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'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:fixnum/fixnum.dart'; - -class DocumentService { - // unused now. - Future> createDocument({ - required ViewPB view, - }) async { - final canOpen = await openDocument(documentId: view.id); - if (canOpen.isSuccess) { - return FlowyResult.success(null); - } - final payload = CreateDocumentPayloadPB()..documentId = view.id; - final result = await DocumentEventCreateDocument(payload).send(); - return result; - } - - Future> openDocument({ - required String documentId, - }) async { - final payload = OpenDocumentPayloadPB()..documentId = documentId; - final result = await DocumentEventOpenDocument(payload).send(); - return result; - } - - Future> getDocument({ - required String documentId, - }) async { - final payload = OpenDocumentPayloadPB()..documentId = documentId; - final result = await DocumentEventGetDocumentData(payload).send(); - 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, - }) async { - final block = document.blocks[blockId]; - - if (block != null) { - return FlowyResult.success(block); - } - - return FlowyResult.failure( - FlowyError( - msg: 'Block($blockId) not found in Document(${document.pageId})', - ), - ); - } - - Future> closeDocument({ - required String viewId, - }) async { - final payload = ViewIdPB()..value = viewId; - final result = await FolderEventCloseView(payload).send(); - return result; - } - - Future> applyAction({ - required String documentId, - required Iterable actions, - }) async { - final payload = ApplyActionPayloadPB( - documentId: documentId, - actions: actions, - ); - final result = await DocumentEventApplyAction(payload).send(); - return result; - } - - /// Creates a new external text. - /// - /// Normally, it's used to the block that needs sync long text. - /// - /// the delta parameter is the json representation of the delta. - Future> createExternalText({ - required String documentId, - required String textId, - String? delta, - }) async { - final payload = TextDeltaPayloadPB( - documentId: documentId, - textId: textId, - delta: delta, - ); - final result = await DocumentEventCreateText(payload).send(); - return result; - } - - /// Updates the external text. - /// - /// this function is compatible with the [createExternalText] function. - /// - /// the delta parameter is the json representation of the delta too. - Future> updateExternalText({ - required String documentId, - required String textId, - String? delta, - }) async { - final payload = TextDeltaPayloadPB( - documentId: documentId, - textId: textId, - delta: delta, - ); - final result = await DocumentEventApplyTextDeltaEvent(payload).send(); - return result; - } - - /// Upload a file to the cloud storage. - Future> uploadFile({ - required String localFilePath, - required String documentId, - }) 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')); - }, - ); - } - - /// Download a file from the cloud storage. - Future> downloadFile({ - required String url, - }) async { - final workspace = await FolderEventReadCurrentWorkspace().send(); - return workspace.fold((l) async { - final payload = DownloadFilePB( - url: url, - ); - final result = await DocumentEventDownloadFile(payload).send(); - return result; - }, (r) async { - return FlowyResult.failure(FlowyError(msg: 'Workspace not found')); - }); - } - - /// Sync the awareness states - /// For example, the cursor position, selection, who is viewing the document. - Future> syncAwarenessStates({ - required String documentId, - Selection? selection, - String? metadata, - }) async { - final payload = UpdateDocumentAwarenessStatePB( - documentId: documentId, - selection: convertSelectionToAwarenessSelection(selection), - metadata: metadata, - ); - - final result = await DocumentEventSetAwarenessState(payload).send(); - return result; - } - - DocumentAwarenessSelectionPB? convertSelectionToAwarenessSelection( - Selection? selection, - ) { - if (selection == null) { - return null; - } - return DocumentAwarenessSelectionPB( - start: DocumentAwarenessPositionPB( - offset: Int64(selection.startIndex), - path: selection.start.path.map((e) => Int64(e)), - ), - end: DocumentAwarenessPositionPB( - offset: Int64(selection.endIndex), - path: selection.end.path.map((e) => Int64(e)), - ), - ); - } -} 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 deleted file mode 100644 index 7254539809..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_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-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'document_sync_bloc.freezed.dart'; - -class DocumentSyncBloc extends Bloc { - DocumentSyncBloc({ - required this.view, - }) : _syncStateListener = DocumentSyncStateListener(id: view.id), - super(DocumentSyncBlocState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - final userProfile = await getIt().getUser().then( - (result) => result.fold( - (l) => l, - (r) => null, - ), - ); - emit( - state.copyWith( - shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server, - ), - ); - _syncStateListener.start( - didReceiveSyncState: (syncState) { - add(DocumentSyncEvent.syncStateChanged(syncState)); - }, - ); - - final isNetworkConnected = await _connectivity - .checkConnectivity() - .then((value) => value != ConnectivityResult.none); - emit(state.copyWith(isNetworkConnected: isNetworkConnected)); - - connectivityStream = - _connectivity.onConnectivityChanged.listen((result) { - add(DocumentSyncEvent.networkStateChanged(result)); - }); - }, - syncStateChanged: (syncState) { - emit(state.copyWith(syncState: syncState.value)); - }, - networkStateChanged: (result) { - emit( - state.copyWith( - isNetworkConnected: result != ConnectivityResult.none, - ), - ); - }, - ); - }, - ); - } - - final ViewPB view; - final DocumentSyncStateListener _syncStateListener; - final _connectivity = Connectivity(); - - StreamSubscription? connectivityStream; - - @override - Future close() async { - await connectivityStream?.cancel(); - await _syncStateListener.stop(); - return super.close(); - } -} - -@freezed -class DocumentSyncEvent with _$DocumentSyncEvent { - const factory DocumentSyncEvent.initial() = Initial; - const factory DocumentSyncEvent.syncStateChanged( - DocumentSyncStatePB syncState, - ) = syncStateChanged; - const factory DocumentSyncEvent.networkStateChanged( - ConnectivityResult result, - ) = NetworkStateChanged; -} - -@freezed -class DocumentSyncBlocState with _$DocumentSyncBlocState { - const factory DocumentSyncBlocState({ - required DocumentSyncState syncState, - @Default(true) bool isNetworkConnected, - @Default(false) bool shouldShowIndicator, - }) = _DocumentSyncState; - - factory DocumentSyncBlocState.initial() => const DocumentSyncBlocState( - syncState: DocumentSyncState.Syncing, - ); -} 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..79eb3aa7be 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,17 +1,21 @@ -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/plugins/document/application/doc_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + show + EditorState, + Transaction, + Operation, + InsertOperation, + UpdateOperation, + DeleteOperation, + PathExtensions, + Node, + composeAttributes; import 'package:collection/collection.dart'; -import 'package:nanoid/nanoid.dart'; +import 'dart:async'; -const kExternalTextType = 'text'; +import 'package:nanoid/nanoid.dart'; /// Uses to adjust the data structure between the editor and the backend. /// @@ -27,126 +31,27 @@ class TransactionAdapter { final String documentId; 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()}'); - } - - final actions = transactionToBlockActions(transaction, editorState); - final textActions = filterTextDeltaActions(actions); - - final actionCostTime = stopwatch.elapsedMilliseconds; - for (final textAction in textActions) { - final payload = textAction.textDeltaPayloadPB!; - final type = textAction.textDeltaType; - if (type == TextDeltaType.create) { - await documentService.createExternalText( - documentId: payload.documentId, - textId: payload.textId, - delta: payload.delta, - ); - if (enableDocumentInternalLog) { - Log.info( - '[editor_transaction_adapter] create external text: id: ${payload.textId} delta: ${payload.delta}', - ); - } - } else if (type == TextDeltaType.update) { - await documentService.updateExternalText( - documentId: payload.documentId, - textId: payload.textId, - delta: payload.delta, - ); - if (enableDocumentInternalLog) { - Log.info( - '[editor_transaction_adapter] update external text: id: ${payload.textId} delta: ${payload.delta}', - ); - } - } - } - - final blockActions = filterBlockActions(actions); - - for (final action in blockActions) { - if (enableDocumentInternalLog) { - Log.info( - '[editor_transaction_adapter] action => ${action.toProto3Json()}', - ); - } - } - - 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', - ); - } - } - - List transactionToBlockActions( - Transaction transaction, - EditorState editorState, - ) { - return transaction.operations - .map((op) => op.toBlockAction(editorState, documentId)) - .nonNulls + // Log.debug('transaction => ${transaction.toJson()}'); + final actions = transaction.operations + .map((op) => op.toBlockAction(editorState)) + .whereNotNull() .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); + // Log.debug('actions => $actions'); + await documentService.applyAction( + documentId: documentId, + actions: actions, + ); } } -extension BlockAction on Operation { - List toBlockAction( - EditorState editorState, - String documentId, - ) { +extension on Operation { + List toBlockAction(EditorState editorState) { final op = this; if (op is InsertOperation) { - return op.toBlockAction(editorState, documentId); + return op.toBlockAction(editorState); } else if (op is UpdateOperation) { - return op.toBlockAction(editorState, documentId); + return op.toBlockAction(editorState); } else if (op is DeleteOperation) { return op.toBlockAction(editorState); } @@ -155,108 +60,48 @@ extension BlockAction on Operation { } extension on InsertOperation { - List toBlockAction( - EditorState editorState, - String documentId, { - Node? previousNode, - }) { - Path currentPath = path; - final List actions = []; + List toBlockAction(EditorState editorState) { + final List actions = []; + // store the previous node for continuous insertion. + // because the backend needs to know the previous node's id. + Node? previousNode; for (final node in nodes) { - if (node.type == AiWriterBlockKeys.type) { - continue; - } - - final parentId = node.parent?.id ?? - editorState.getNodeAtPath(currentPath.parent)?.id ?? + final parentId = + node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? ''; + var prevId = previousNode?.id ?? + editorState.getNodeAtPath(path.previous)?.id ?? ''; assert(parentId.isNotEmpty); - - String prevId = ''; - // 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 ?? - ''; + if (path.equals(path.previous)) { + 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); - - textDeltaPayloadPB = TextDeltaPayloadPB( - documentId: documentId, - textId: textId, - delta: jsonEncode(node.delta!.toJson()), - ); - - // sync the text id to the node - node.externalValues = ExternalValues( - externalId: textId, - externalType: kExternalTextType, - ); - } - - // 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(10)) ..parentId = parentId ..prevId = prevId; - - // pass the external text id to the payload. - if (textDeltaPayloadPB != null) { - payload.textId = textDeltaPayloadPB.textId; - } - assert(payload.block.childrenId.isNotEmpty); - final blockActionPB = BlockActionPB() - ..action = BlockActionTypePB.Insert - ..payload = payload; - actions.add( - BlockActionWrapper( - blockActionPB: blockActionPB, - textDeltaPayloadPB: textDeltaPayloadPB, - textDeltaType: TextDeltaType.create, - ), + BlockActionPB() + ..action = BlockActionTypePB.Insert + ..payload = payload, ); if (node.children.isNotEmpty) { - Node? prevChild; - for (final child in node.children) { - actions.addAll( - InsertOperation(currentPath + child.path, [child]).toBlockAction( - editorState, - documentId, - previousNode: prevChild, - ), - ); - prevChild = child; - } + final childrenActions = node.children + .map((e) => InsertOperation(e.path, [e]).toBlockAction(editorState)) + .expand((element) => element); + actions.addAll(childrenActions); } previousNode = node; - currentPath = currentPath.next; } return actions; } } extension on UpdateOperation { - List toBlockAction( - EditorState editorState, - String documentId, - ) { - final List actions = []; + List toBlockAction(EditorState editorState) { + final List actions = []; // if the attributes are both empty, we don't need to update if (const DeepCollectionEquality().equals(attributes, oldAttributes)) { @@ -270,155 +115,23 @@ extension on UpdateOperation { final parentId = node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? ''; 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 payload = BlockActionPayloadPB() ..block = node.toBlock( parentId: parentId, - attributes: composedAttributes, + attributes: composeAttributes(oldAttributes, attributes), ) ..parentId = parentId; - final blockActionPB = BlockActionPB() - ..action = BlockActionTypePB.Update - ..payload = payload; - - final textId = (node.externalValues as ExternalValues?)?.externalId; - 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 - ? null - : TextDeltaPayloadPB( - documentId: documentId, - textId: textId, - delta: jsonEncode(correctedTextDelta), - ); - - node.externalValues = ExternalValues( - externalId: textId, - externalType: kExternalTextType, - ); - - 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, - textDeltaPayloadPB: textDeltaPayloadPB, - textDeltaType: TextDeltaType.create, - ), - ); - } 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 - ? null - : TextDeltaPayloadPB( - documentId: documentId, - textId: textId, - delta: jsonEncode(correctedDiff), - ); - - 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, - textDeltaPayloadPB: textDeltaPayloadPB, - textDeltaType: TextDeltaType.update, - ), - ); - } - + actions.add( + BlockActionPB() + ..action = BlockActionTypePB.Update + ..payload = payload, + ); 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 { - List toBlockAction(EditorState editorState) { + List toBlockAction(EditorState editorState) { final List actions = []; for (final node in nodes) { final parentId = @@ -435,26 +148,6 @@ extension on DeleteOperation { ..payload = payload, ); } - return actions - .map((e) => BlockActionWrapper(blockActionPB: e)) - .toList(growable: false); + return actions; } } - -enum TextDeltaType { - none, - create, - update, -} - -class BlockActionWrapper { - BlockActionWrapper({ - required this.blockActionPB, - this.textDeltaType = TextDeltaType.none, - this.textDeltaPayloadPB, - }); - - final BlockActionPB blockActionPB; - final TextDeltaPayloadPB? textDeltaPayloadPB; - final TextDeltaType textDeltaType; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart b/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart index a6497bf6de..710ac383f7 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 'doc_bloc.dart'; +export 'doc_service.dart'; +export 'share_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart new file mode 100644 index 0000000000..c4dc32ffdb --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/share_bloc.dart @@ -0,0 +1,60 @@ +import 'dart:io'; +import 'package:appflowy/workspace/application/export/document_exporter.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; +part 'share_bloc.freezed.dart'; + +class DocShareBloc extends Bloc { + DocShareBloc({ + required this.view, + }) : super(const DocShareState.initial()) { + on(_onShareMarkdown); + } + + final ViewPB view; + + Future _onShareMarkdown( + ShareMarkdown event, + Emitter emit, + ) async { + emit(const DocShareState.loading()); + + final documentExporter = DocumentExporter(view); + final result = await documentExporter.export(DocumentExportType.markdown); + emit( + DocShareState.finish( + result.fold( + (error) => right(error), + (markdown) => left(_saveMarkdownToPath(markdown, event.path)), + ), + ), + ); + } + + ExportDataPB _saveMarkdownToPath(String markdown, String path) { + File(path).writeAsStringSync(markdown); + return ExportDataPB() + ..data = markdown + ..exportType = ExportType.Markdown; + } +} + +@freezed +class DocShareEvent with _$DocShareEvent { + const factory DocShareEvent.shareMarkdown(String path) = ShareMarkdown; + const factory DocShareEvent.shareText() = ShareText; + const factory DocShareEvent.shareLink() = ShareLink; +} + +@freezed +class DocShareState with _$DocShareState { + const factory DocShareState.initial() = _Initial; + const factory DocShareState.loading() = _Loading; + const factory DocShareState.finish( + Either successOrFail, + ) = _Finish; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 4ebc6f1b47..f1ae0ca431 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,27 +1,16 @@ -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/more/cubit/document_appearance_cubit.dart'; +import 'package:appflowy/plugins/document/presentation/more/more_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'; -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/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -30,172 +19,120 @@ class DocumentPluginBuilder extends PluginBuilder { Plugin build(dynamic data) { if (data is ViewPB) { return DocumentPlugin(pluginType: pluginType, view: data); + } else { + throw FlowyPluginException.invalidData; } - - throw FlowyPluginException.invalidData; } @override String get menuName => LocaleKeys.document_menuName.tr(); @override - FlowySvgData get icon => FlowySvgs.icon_document_s; + String get menuIcon => "editor/documents"; @override - PluginType get pluginType => PluginType.document; + PluginType get pluginType => PluginType.editor; @override - ViewLayoutPB get layoutType => ViewLayoutPB.Document; + ViewLayoutPB? get layoutType => ViewLayoutPB.Document; } -class DocumentPlugin extends Plugin { - DocumentPlugin({ - required ViewPB view, - required PluginType pluginType, - this.initialSelection, - this.initialBlockId, - }) : notifier = ViewPluginNotifier(view: view) { - _pluginType = pluginType; - } - +class DocumentPlugin extends Plugin { late PluginType _pluginType; - late final ViewInfoBloc _viewInfoBloc; + final DocumentAppearanceCubit _documentAppearanceCubit = + DocumentAppearanceCubit(); @override final ViewPluginNotifier notifier; - // the initial selection of the document - final Selection? initialSelection; - - // the initial block id of the document - final String? initialBlockId; + DocumentPlugin({ + required PluginType pluginType, + required ViewPB view, + bool listenOnViewChanged = false, + Key? key, + }) : notifier = ViewPluginNotifier(view: view) { + _pluginType = pluginType; + _documentAppearanceCubit.fetch(); + } @override - PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder( - bloc: _viewInfoBloc, - notifier: notifier, - initialSelection: initialSelection, - initialBlockId: initialBlockId, - ); + void dispose() { + _documentAppearanceCubit.close(); + super.dispose(); + } + + @override + PluginWidgetBuilder get widgetBuilder { + return DocumentPluginWidgetBuilder( + notifier: notifier, + documentAppearanceCubit: _documentAppearanceCubit, + ); + } @override PluginType get pluginType => _pluginType; @override PluginId get id => notifier.view.id; - - @override - void init() { - _viewInfoBloc = ViewInfoBloc(view: notifier.view) - ..add(const ViewInfoEvent.started()); - } - - @override - void dispose() { - _viewInfoBloc.close(); - notifier.dispose(); - } } class DocumentPluginWidgetBuilder extends PluginWidgetBuilder with NavigationItem { - DocumentPluginWidgetBuilder({ - 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; + DocumentAppearanceCubit documentAppearanceCubit; + + DocumentPluginWidgetBuilder({ + required this.notifier, + required this.documentAppearanceCubit, + Key? key, + }); @override EdgeInsets get contentPadding => EdgeInsets.zero; @override - Widget buildWidget({ - required PluginContext context, - required bool shrinkWrap, - Map? data, - }) { + Widget buildWidget({PluginContext? context}) { notifier.isDeleted.addListener(() { - final deletedView = notifier.isDeleted.value; - if (deletedView != null && deletedView.hasIndex()) { - deletedViewIndex = deletedView.index; - } + notifier.isDeleted.value.fold(() => null, (deletedView) { + if (deletedView.hasIndex()) { + deletedViewIndex = deletedView.index; + } + }); }); - 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, + return BlocProvider.value( + value: documentAppearanceCubit, child: BlocBuilder( - builder: (_, state) => DocumentPage( - key: ValueKey(view.id), - view: view, - onDeleted: () => context.onDeleted?.call(view, deletedViewIndex), - initialSelection: initialSelection, - initialBlockId: blockId, - fixedTitle: fixedTitle, - tabs: tabs, - ), + builder: (_, state) { + return DocumentPage( + view: view, + onDeleted: () => context?.onDeleted(view, deletedViewIndex), + key: ValueKey(view.id), + ); + }, ), ); } @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 get leftBarItem => ViewLeftBarItem(view: view); @override Widget? get rightBarItem { - return BlocProvider.value( - value: bloc, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ...FeatureFlag.syncDocument.isOn - ? [ - DocumentCollaborators( - key: ValueKey('collaborators_${view.id}'), - width: 120, - height: 32, - view: view, - ), - const HSpace(16), - ] - : [const HSpace(8)], - ShareButton( - key: ValueKey('share_button_${view.id}'), - view: view, - ), - const HSpace(10), - ViewFavoriteButton( - key: ValueKey('favorite_button_${view.id}'), - view: view, - ), - const HSpace(4), - MoreViewActions(view: view), - ], - ), + return Row( + children: [ + DocumentShareButton( + key: ValueKey(view.id), + view: view, + ), + const SizedBox(width: 10), + BlocProvider.value( + value: documentAppearanceCubit, + child: const DocumentMoreButton(), + ), + ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 8716bb7ae2..2e448c99bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,381 +1,156 @@ +import 'dart:convert'; +import 'dart:io'; + 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/application/doc_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_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/plugins/document/presentation/export_page_widget.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/util/base64_string.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' + hide DocumentEvent; +import 'package:appflowy_backend/protobuf/flowy-folder2/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/widget/error_page.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 'package:path/path.dart' as p; class DocumentPage extends StatefulWidget { const DocumentPage({ super.key, - required this.view, required this.onDeleted, - required this.tabs, - this.initialSelection, - this.initialBlockId, - this.fixedTitle, + required this.view, }); - final ViewPB view; final VoidCallback onDeleted; - final Selection? initialSelection; - final String? initialBlockId; - final String? fixedTitle; - final List tabs; + final ViewPB view; @override State createState() => _DocumentPageState(); } -class _DocumentPageState extends State - with WidgetsBindingObserver { +class _DocumentPageState extends State { + late final DocumentBloc documentBloc; EditorState? editorState; - Selection? initialSelection; - late final documentBloc = DocumentBloc(documentId: widget.view.id) - ..add(const DocumentEvent.initial()); @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); + + documentBloc = getIt(param1: widget.view) + ..add(const DocumentEvent.initial()); + + // The appflowy editor use Intl as localization, set the default language as fallback. + Intl.defaultLocale = 'en_US'; } @override void dispose() { - WidgetsBinding.instance.removeObserver(this); documentBloc.close(); - super.dispose(); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.paused || - state == AppLifecycleState.detached) { - documentBloc.add(const DocumentEvent.clearAwarenessStates()); - } else if (state == AppLifecycleState.resumed) { - documentBloc.add(const DocumentEvent.syncAwarenessStates()); - } - } - @override Widget build(BuildContext context) { - return MultiBlocProvider( - 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; - } - 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)); - } - - 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 BlocProvider.value( + value: documentBloc, + child: BlocBuilder( + builder: (context, state) { + return state.loadingState.when( + loading: () => const SizedBox.shrink(), + finish: (result) => result.fold( + (error) => FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + (data) { + if (state.forceClose) { + widget.onDeleted(); + return const SizedBox.shrink(); + } else if (documentBloc.editorState == null) { + return Center( + child: ExportPageWidget( + onTap: () async => await _exportPage(data), + ), + ); + } else { + editorState = documentBloc.editorState!; + return _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); - - final Widget child; - if (UniversalPlatform.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, - ), - ); - } 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() - : '', - ), - ); - } - - 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), - ], - ), + Widget _buildEditorPage(BuildContext context, DocumentState state) { + final appflowyEditorPage = AppFlowyEditorPage( + editorState: editorState!, + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.symmetric(horizontal: 50), ), + header: _buildCoverAndIcon(context), + ); + return Column( + children: [ + if (state.isDeleted) _buildBanner(context), + Expanded( + child: appflowyEditorPage, + ), + ], ); } - Widget buildBanner(BuildContext context) { + Widget _buildBanner(BuildContext context) { return DocumentBanner( - viewName: widget.view.nameOrDefault, - onRestore: () => - context.read().add(const DocumentEvent.restorePage()), - onDelete: () => context - .read() - .add(const DocumentEvent.deletePermanently()), + onRestore: () => documentBloc.add(const DocumentEvent.restorePage()), + onDelete: () => documentBloc.add(const DocumentEvent.deletePermanently()), ); } - Widget buildCoverAndIcon(BuildContext context, DocumentState state) { - final editorState = state.editorState; - final userProfilePB = state.userProfilePB; - if (editorState == null || userProfilePB == null) { - return const SizedBox.shrink(); + Widget _buildCoverAndIcon(BuildContext context) { + if (editorState == null) { + return const Placeholder(); } - - if (UniversalPlatform.isMobile) { - return DocumentImmersiveCover( - fixedTitle: widget.fixedTitle, - view: widget.view, - tabs: widget.tabs, - userProfilePB: userProfilePB, - ); - } - - final page = editorState.document.root; - return DocumentCoverWidget( + final page = editorState!.document.root; + return CoverImageNodeWidget( node: page, - tabs: widget.tabs, - editorState: editorState, - view: widget.view, - onIconChanged: (icon) async => ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: icon, + editorState: editorState!, + ); + } + + Future _exportPage(DocumentDataPB data) async { + final picker = getIt(); + final dir = await picker.getDirectoryPath(); + if (dir == null) { + return; + } + final path = p.join(dir, '${documentBloc.view.name}.json'); + const encoder = JsonEncoder.withIndent(' '); + final json = encoder.convert(data.toProto3Json()); + await File(path).writeAsString(json.base64.base64); + + _showMessage('Export success to $path'); + } + + void _showMessage(String message) { + if (!mounted) { + return; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: FlowyText(message), ), ); } - - 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; - if (editorState == null) { - return; - } - - final Path? path = _getPathFromAction(action, editorState); - if (path != null) { - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: path)), - ); - } - } - - 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; - } - - 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, - ), - ); - } - } - - 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..49a76846a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart @@ -1,40 +1,35 @@ -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; + const DocumentBanner({ + required this.onRestore, + required this.onDelete, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; return ConstrainedBox( constraints: const BoxConstraints(minHeight: 60), child: Container( width: double.infinity, - color: colorScheme.surfaceContainerHighest, + color: Theme.of(context).colorScheme.primary, child: FittedBox( + alignment: Alignment.center, fit: BoxFit.scaleDown, child: Row( children: [ FlowyText.medium( LocaleKeys.deletePagePrompt_text.tr(), - color: colorScheme.tertiary, - fontSize: 14, + color: Colors.white, ), const HSpace(20), BaseStyledButton( @@ -42,14 +37,15 @@ class DocumentBanner extends StatelessWidget { minHeight: 40, contentPadding: EdgeInsets.zero, bgColor: Colors.transparent, - highlightColor: Theme.of(context).colorScheme.onErrorContainer, - outlineColor: colorScheme.tertiaryContainer, + hoverColor: Theme.of(context).colorScheme.primary, + highlightColor: Theme.of(context).colorScheme.primaryContainer, + outlineColor: Colors.white, borderRadius: Corners.s8Border, onPressed: onRestore, child: FlowyText.medium( LocaleKeys.deletePagePrompt_restore.tr(), - color: colorScheme.tertiary, - fontSize: 13, + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 14, ), ), const HSpace(20), @@ -58,23 +54,15 @@ class DocumentBanner extends StatelessWidget { minHeight: 40, contentPadding: EdgeInsets.zero, bgColor: Colors.transparent, - highlightColor: Theme.of(context).colorScheme.error, - outlineColor: colorScheme.tertiaryContainer, + hoverColor: Theme.of(context).colorScheme.primaryContainer, + highlightColor: Theme.of(context).colorScheme.primary, + outlineColor: Colors.white, 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, - fontSize: 13, + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 14, ), ), ], 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 deleted file mode 100644 index 1be5a41d81..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:avatar_stack/avatar_stack.dart'; -import 'package:avatar_stack/positions.dart'; -import 'package:flutter/material.dart'; - -class CollaboratorAvatarStack extends StatelessWidget { - const CollaboratorAvatarStack({ - super.key, - required this.avatars, - this.settings, - this.infoWidgetBuilder, - this.width, - this.height, - this.borderWidth, - this.borderColor, - this.backgroundColor, - required this.plusWidgetBuilder, - }); - - final List avatars; - final Positions? settings; - final InfoWidgetBuilder? infoWidgetBuilder; - final double? width; - final double? height; - final double? borderWidth; - final Color? borderColor; - final Color? backgroundColor; - final Widget Function(int value, BorderSide border) plusWidgetBuilder; - - @override - Widget build(BuildContext context) { - final settings = this.settings ?? - RestrictedPositions( - maxCoverage: 0.4, - minCoverage: 0.3, - align: StackAlign.right, - laying: StackLaying.first, - ); - - final border = BorderSide( - color: borderColor ?? Theme.of(context).dividerColor, - width: borderWidth ?? 2.0, - ); - - return SizedBox( - height: height, - width: width, - child: WidgetStack( - positions: settings, - buildInfoWidget: (value, _) => plusWidgetBuilder(value, border), - stackedWidgets: avatars - .map( - (avatar) => CircleAvatar( - backgroundColor: border.color, - child: Padding( - padding: EdgeInsets.all(border.width), - child: avatar, - ), - ), - ) - .toList(), - ), - ); - } -} 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 deleted file mode 100644 index d4a6815e32..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart +++ /dev/null @@ -1,108 +0,0 @@ -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:avatar_stack/avatar_stack.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 DocumentCollaborators extends StatelessWidget { - const DocumentCollaborators({ - super.key, - required this.height, - required this.width, - required this.view, - this.padding, - this.fontSize, - }); - - final ViewPB view; - final double height; - final double width; - final EdgeInsets? padding; - final double? fontSize; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DocumentCollaboratorsBloc(view: view) - ..add(const DocumentCollaboratorsEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final collaborators = state.collaborators; - if (!state.shouldShowIndicator || collaborators.isEmpty) { - return const SizedBox.shrink(); - } - - return Padding( - padding: padding ?? EdgeInsets.zero, - child: CollaboratorAvatarStack( - height: height, - width: width, - borderWidth: 1.0, - plusWidgetBuilder: (value, border) { - final lastXCollaborators = collaborators.sublist( - collaborators.length - value, - ); - return BorderedCircleAvatar( - border: border, - backgroundColor: Theme.of(context).hoverColor, - child: FittedBox( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: FlowyTooltip( - message: lastXCollaborators - .map((e) => e.userName) - .join('\n'), - child: FlowyText( - '+$value', - fontSize: fontSize, - color: Colors.black, - ), - ), - ), - ), - ); - }, - avatars: [ - ...collaborators.map( - (c) => _UserAvatar(fontSize: fontSize, user: c, width: width), - ), - ], - ), - ); - }, - ), - ); - } -} - -class _UserAvatar extends StatelessWidget { - const _UserAvatar({ - this.fontSize, - required this.user, - required this.width, - }); - - final DocumentAwarenessMetadata user; - final double? fontSize; - final double width; - - @override - Widget build(BuildContext 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), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart deleted file mode 100644 index 5e7eefc24e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ /dev/null @@ -1,1098 +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/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/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_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({ - required BuildContext context, - required EditorState editorState, - required EditorStyleCustomizer styleCustomizer, - SlashMenuItemsBuilder? slashMenuItemsBuilder, - 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, - ); - - // 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 configuration = BlockComponentConfiguration( - padding: (node) { - if (UniversalPlatform.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; - } - - 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); - }, - ); - 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, - ), - TodoListBlockKeys.type: _buildTodoListBlockComponentBuilder( - context, - configuration, - ), - BulletedListBlockKeys.type: _buildBulletedListBlockComponentBuilder( - context, - configuration, - ), - NumberedListBlockKeys.type: _buildNumberedListBlockComponentBuilder( - context, - configuration, - ), - QuoteBlockKeys.type: _buildQuoteBlockComponentBuilder( - context, - configuration, - ), - HeadingBlockKeys.type: _buildHeadingBlockComponentBuilder( - context, - configuration, - styleCustomizer, - customHeadingPadding, - ), - ImageBlockKeys.type: _buildCustomImageBlockComponentBuilder( - context, - configuration, - ), - MultiImageBlockKeys.type: _buildMultiImageBlockComponentBuilder( - context, - configuration, - ), - TableBlockKeys.type: _buildTableBlockComponentBuilder( - context, - configuration, - ), - TableCellBlockKeys.type: _buildTableCellBlockComponentBuilder( - context, - configuration, - ), - DatabaseBlockKeys.gridType: _buildDatabaseViewBlockComponentBuilder( - context, - configuration, - ), - DatabaseBlockKeys.boardType: _buildDatabaseViewBlockComponentBuilder( - context, - configuration, - ), - DatabaseBlockKeys.calendarType: _buildDatabaseViewBlockComponentBuilder( - context, - configuration, - ), - CalloutBlockKeys.type: _buildCalloutBlockComponentBuilder( - context, - configuration, - ), - DividerBlockKeys.type: _buildDividerBlockComponentBuilder( - context, - configuration, - editorState, - ), - MathEquationBlockKeys.type: _buildMathEquationBlockComponentBuilder( - context, - configuration, - ), - CodeBlockKeys.type: _buildCodeBlockComponentBuilder( - context, - configuration, - styleCustomizer, - ), - AiWriterBlockKeys.type: _buildAIWriterBlockComponentBuilder( - context, - configuration, - ), - 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, - ), - 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 = { - ...standardBlockComponentBuilderMap, - ...customBlockComponentBuilderMap, - }; - - 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 deleted file mode 100644 index fce8b4c16e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy_editor/appflowy_editor.dart'; - -enum EditorNotificationType { - none, - undo, - redo, - exitEditing, - paste, - dragStart, - dragEnd, - turnInto, -} - -class EditorNotification { - const EditorNotification({required this.type}); - - 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); - - final EditorNotificationType type; - - void post() => _notifier.value = type; - - static void addListener(ValueChanged listener) { - _notifier.addListener(() => listener(_notifier.value)); - } - - static void removeListener(ValueChanged listener) { - _notifier.removeListener(() => listener(_notifier.value)); - } - - static void dispose() => _notifier.dispose(); -} 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..30d8af6664 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,12 @@ -import 'dart:ui' as ui; - -import 'package:appflowy/core/helpers/url_launcher.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/background_color/theme_background_color.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart'; +import 'package:appflowy/plugins/document/application/doc_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_list.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.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_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:collection/collection.dart'; -import 'package:flowy_infra/theme_extension.dart'; +import 'package:appflowy_editor/appflowy_editor.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/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'; /// Wrapper for the appflowy editor. class AppFlowyEditorPage extends StatefulWidget { @@ -49,10 +18,6 @@ class AppFlowyEditorPage extends StatefulWidget { this.scrollController, this.autoFocus, required this.styleCustomizer, - this.showParagraphPlaceholder, - this.placeholderText, - this.initialSelection, - this.useViewInfoBloc = true, }); final Widget? header; @@ -61,275 +26,87 @@ class AppFlowyEditorPage extends StatefulWidget { final bool shrinkWrap; final bool? autoFocus; final EditorStyleCustomizer styleCustomizer; - final ShowPlaceholder? showParagraphPlaceholder; - final String Function(Node)? placeholderText; - - /// Used to provide an initial selection on Page-load - final Selection? initialSelection; - - final bool useViewInfoBloc; @override 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 = [ - ...commandShortcutEvents, - ..._buildFindAndReplaceCommands(), + final List commandShortcutEvents = [ + ...codeBlockCommands, + ...standardCommandShortcutEvents, ]; final List toolbarItems = [ - improveWritingItem, - group0PaddingItem, - aiWriterItem, - customTextHeadingItem, - buildPaddingPlaceholderItem( - 1, - isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, - ), - ...customMarkdownFormatItems, - group1PaddingItem, - customTextColorItem, - group1PaddingItem, - customHighlightColorItem, - customInlineCodeItem, - suggestionsItem, - customLinkItem, - group4PaddingItem, - customTextAlignItem, - moreOptionItem, + smartEditItem, + paragraphItem, + ...headingItems, + ...markdownFormatItems, + quoteItem, + bulletedListItem, + numberedListItem, + linkItem, + textColorItem, + highlightColorItem, ]; - List get characterShortcutEvents { - return buildCharacterShortcutEvents( - context, - documentBloc, - styleCustomizer, - inlineActionsService, - (editorState, node) => _customSlashMenuItems( - editorState: editorState, - node: node, - ), - ); - } + late final slashMenuItems = [ + inlineGridMenuItem(documentBloc), + referencedGridMenuItem, + inlineBoardMenuItem(documentBloc), + referencedBoardMenuItem, + inlineCalendarMenuItem(documentBloc), + referencedCalendarMenuItem, + calloutItem, + mathEquationItem, + codeBlockItem, + emojiMenuItem, + autoGeneratorMenuItem, + ]; + + late final Map blockComponentBuilders = + _customAppFlowyBlockComponentBuilders(); + List get characterShortcutEvents => [ + // code block + ...codeBlockCharacterEvents, + + // toggle list + // formatGreaterToToggleList, + + // customize the slash menu command + customSlashCommand( + slashMenuItems, + style: styleCustomizer.selectionMenuStyleBuilder(), + ), + + ...standardCharacterShortcutEvents + ..removeWhere( + (element) => element == slashCommand, + ), // remove the default slash command. + ]; + + late final showSlashMenu = customSlashCommand( + slashMenuItems, + shouldInsertSlash: false, + style: styleCustomizer.selectionMenuStyleBuilder(), + ).handler; 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(), - shouldInsertSlash: false, - style: styleCustomizer.selectionMenuStyleBuilder(), - supportSlashMenuNodeTypes: supportSlashMenuNodeTypes, - ).handler(editorState); - - AFFocusManager? focusManager; - - AppLifecycleState? lifecycleState = WidgetsBinding.instance.lifecycleState; - List previousSelections = []; - @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); - - if (widget.useViewInfoBloc) { - viewInfoBloc.add( - ViewInfoEvent.registerEditorState(editorState: widget.editorState), - ); - } - - _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); - }; - effectiveScrollController = widget.scrollController ?? ScrollController(); - // disable the color parse in the HTML decoder. - DocumentHTMLDecoder.enableColorParse = false; - - editorScrollController = EditorScrollController( - editorState: widget.editorState, - shrinkWrap: widget.shrinkWrap, - scrollController: effectiveScrollController, - ); - - 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; - } - - focusManager = AFFocusManager.maybeOf(context); - focusManager?.loseFocusNotifier.addListener(_loseFocus); - - _scrollToSelectionIfNeeded(); - - widget.editorState.service.keyboardService?.registerInterceptor( - editorKeyboardInterceptor, - ); - }); - } - - 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); - if (focusManager != currFocusManager) { - focusManager?.loseFocusNotifier.removeListener(_loseFocus); - 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) { - viewInfoBloc.add(const ViewInfoEvent.unregisterEditorState()); - } - - SystemChannels.textInput.invokeMethod('TextInput.hide'); - if (widget.scrollController == null) { effectiveScrollController.dispose(); } - inlineActionsService.dispose(); - editorScrollController.dispose(); super.dispose(); } @@ -339,302 +116,192 @@ class _AppFlowyEditorPageState extends State final (bool autoFocus, Selection? selection) = _computeAutoFocusParameters(); - final isRTL = - context.read().state.layoutDirection == - LayoutDirection.rtlLayout; - final textDirection = isRTL ? ui.TextDirection.rtl : ui.TextDirection.ltr; - - _setRTLToolbarItems( - context.read().state.enableRtlToolbarItems, + final editor = AppFlowyEditor.custom( + editorState: widget.editorState, + editable: true, + shrinkWrap: widget.shrinkWrap, + scrollController: effectiveScrollController, + // setup the auto focus parameters + autoFocus: widget.autoFocus ?? autoFocus, + focusedSelection: selection, + // setup the theme + editorStyle: styleCustomizer.style(), + // customize the block builder + blockComponentBuilders: blockComponentBuilders, + // customize the shortcuts + characterShortcutEvents: characterShortcutEvents, + commandShortcutEvents: commandShortcutEvents, + header: widget.header, ); - 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, - focusedSelection: selection, - // setup the theme - editorStyle: styleCustomizer.style(), - // customize the block builders - blockComponentBuilders: buildBlockComponentBuilders( - slashMenuItemsBuilder: (editorState, node) => _customSlashMenuItems( - editorState: editorState, - node: node, - ), - context: context, - editorState: widget.editorState, - styleCustomizer: widget.styleCustomizer, - showParagraphPlaceholder: widget.showParagraphPlaceholder, - placeholderText: widget.placeholderText, - ), - // customize the shortcuts - characterShortcutEvents: characterShortcutEvents, - commandShortcutEvents: commandShortcuts, - // 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), - ), - ), - ); - - if (isViewDeleted) { - return editor; - } - - final editorState = widget.editorState; - - if (UniversalPlatform.isMobile) { - return AppFlowyMobileToolbar( - toolbarHeight: 42.0, - editorState: editorState, - toolbarItemsBuilder: (sel) => buildMobileToolbarItems(editorState, sel), - child: MobileFloatingToolbar( - editorState: editorState, - editorScrollController: editorScrollController, - toolbarBuilder: (_, anchor, closeToolbar) => - CustomMobileFloatingToolbar( - editorState: editorState, - anchor: anchor, - closeToolbar: closeToolbar, - ), - floatingToolbarHeight: 32, - child: editor, - ), - ); - } - final appTheme = AppFlowyTheme.of(context); return Center( - child: BlocProvider.value( - value: context.read(), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: double.infinity, + maxHeight: double.infinity, + ), child: FloatingToolbar( - floatingToolbarHeight: 40, - padding: EdgeInsets.symmetric(horizontal: 6), - style: FloatingToolbarStyle( - backgroundColor: Theme.of(context).cardColor, - toolbarActiveColor: Color(0xffe0f8fd), - ), + style: styleCustomizer.floatingToolbarStyleBuilder(), 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, - ), + editorState: widget.editorState, + scrollController: effectiveScrollController, 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, + Map _customAppFlowyBlockComponentBuilders() { + final standardActions = [ + OptionAction.delete, + OptionAction.duplicate, + // OptionAction.divider, + // OptionAction.moveUp, + // OptionAction.moveDown, + ]; + + final configuration = BlockComponentConfiguration( + padding: (_) => const EdgeInsets.symmetric(vertical: 4.0), ); + final customBlockComponentBuilderMap = { + PageBlockKeys.type: PageBlockComponentBuilder(), + ParagraphBlockKeys.type: TextBlockComponentBuilder( + configuration: configuration, + ), + TodoListBlockKeys.type: TodoListBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => 'To-do', + ), + ), + BulletedListBlockKeys.type: BulletedListBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => 'List', + ), + ), + NumberedListBlockKeys.type: NumberedListBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => 'List', + ), + ), + QuoteBlockKeys.type: QuoteBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => 'Quote', + ), + ), + HeadingBlockKeys.type: HeadingBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0), + placeholderText: (node) => + 'Heading ${node.attributes[HeadingBlockKeys.level]}', + ), + textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level), + ), + ImageBlockKeys.type: ImageBlockComponentBuilder( + configuration: configuration, + ), + DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder( + configuration: configuration, + ), + DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder( + configuration: configuration, + ), + DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder( + configuration: configuration, + ), + CalloutBlockKeys.type: CalloutBlockComponentBuilder( + configuration: configuration, + ), + DividerBlockKeys.type: DividerBlockComponentBuilder(), + MathEquationBlockKeys.type: MathEquationBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => const EdgeInsets.symmetric(vertical: 20), + ), + ), + CodeBlockKeys.type: CodeBlockComponentBuilder( + configuration: configuration.copyWith( + textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(), + placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(), + ), + padding: const EdgeInsets.only( + left: 30, + right: 30, + bottom: 36, + ), + ), + AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(), + SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(), + ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(), + }; + + final builders = { + ...standardBlockComponentBuilderMap, + ...customBlockComponentBuilderMap, + }; + + // 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 + ]; + + final supportAlignBuilderType = [ + ImageBlockKeys.type, + ]; + + final colorAction = [ + OptionAction.divider, + OptionAction.color, + ]; + + final alignAction = [ + OptionAction.divider, + OptionAction.align, + ]; + + final List actions = [ + ...standardActions, + if (supportColorBuilderTypes.contains(entry.key)) ...colorAction, + if (supportAlignBuilderType.contains(entry.key)) ...alignAction, + ]; + + builder.showActions = (_) => true; + builder.actionBuilder = (context, state) => BlockActionList( + blockComponentContext: context, + blockComponentState: state, + editorState: widget.editorState, + actions: actions, + showSlashMenu: () => showSlashMenu( + widget.editorState, + ), + ); + } + + return builders; } (bool, Selection?) _computeAutoFocusParameters() { if (widget.editorState.document.isEmpty) { - return (true, Selection.collapsed(Position(path: [0]))); + return (true, Selection.collapse([0], 0)); + } + final nodes = widget.editorState.document.root.children + .where((element) => element.delta != null); + final isAllEmpty = + nodes.isNotEmpty && nodes.every((element) => element.delta!.isEmpty); + if (isAllEmpty) { + return (true, Selection.collapse(nodes.first.path, 0)); } return const (false, null); } - - Future _initializeShortcuts() async { - defaultCommandShortcutEvents; - final settingsShortcutService = SettingsShortcutService(); - final customizeShortcuts = - await settingsShortcutService.getCustomizeShortcuts(); - await settingsShortcutService.updateCommandShortcuts( - commandShortcuts, - customizeShortcuts, - ); - } - - void _setRTLToolbarItems(bool enableRtlToolbarItems) { - final textDirectionItemIds = textDirectionItems.map((e) => e.id); - // clear all the text direction items - toolbarItems.removeWhere((item) => textDirectionItemIds.contains(item.id)); - // only show the rtl item when the layout direction is ltr. - if (enableRtlToolbarItems) { - toolbarItems.addAll(textDirectionItems); - } - } - - List _buildFindAndReplaceCommands() { - return findAndReplaceCommands( - context: context, - style: FindReplaceStyle( - findMenuBuilder: ( - context, - editorState, - localizations, - style, - showReplaceMenu, - onDismiss, - ) => - Material( - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - child: FindAndReplaceMenuWidget( - showReplaceMenu: showReplaceMenu, - editorState: editorState, - onDismiss: onDismiss, - ), - ), - ), - ), - ); - } - - void _customizeBlockComponentBackgroundColorDecorator() { - blockComponentBackgroundColorDecorator = (Node node, String colorString) { - if (mounted && context.mounted) { - return buildEditorCustomizedColor(context, node, colorString); - } - return null; - }; - } - - void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n(); - - Future _focusOnLastEmptyParagraph() async { - final editorState = widget.editorState; - final root = editorState.document.root; - final lastNode = root.children.lastOrNull; - final transaction = editorState.transaction; - if (lastNode == null || - lastNode.delta?.isEmpty == false || - lastNode.type != ParagraphBlockKeys.type) { - transaction.insertNode([root.children.length], paragraphNode()); - transaction.afterSelection = Selection.collapsed( - Position(path: [root.children.length]), - ); - } else { - transaction.afterSelection = Selection.collapsed( - 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( - BuildContext context, - Node node, - String colorString, -) { - if (!context.mounted) { - return null; - } - - // the color string is from FlowyTint. - final tintColor = FlowyTint.values.firstWhereOrNull( - (e) => e.id == colorString, - ); - if (tintColor != null) { - return tintColor.color(context); - } - - final themeColor = themeBackgroundColors[colorString]; - if (themeColor != null) { - return themeColor.color(context); - } - - if (colorString == optionActionColorDefaultColor) { - final defaultColor = node.type == CalloutBlockKeys.type - ? AFThemeExtension.of(context).calloutBGColor - : Colors.transparent; - return defaultColor; - } - - if (colorString == tableCellDefaultColor) { - return AFThemeExtension.of(context).tableCellBGColor; - } - - try { - return colorString.tryToColor(); - } catch (e) { - return null; - } -} - -bool showInAnyTextType(EditorState editorState) { - final selection = editorState.selection; - if (selection == null) { - return false; - } - - final nodes = editorState.getNodesInSelection(selection); - return nodes.any((node) => toolbarItemWhiteList.contains(node.type)); } 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..f05c6244c8 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 @@ -1,22 +1,15 @@ -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/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'; class BlockAddButton extends StatelessWidget { const BlockAddButton({ - super.key, + Key? key, required this.blockComponentContext, required this.blockComponentState, required this.editorState, required this.showSlashMenu, - }); + }) : super(key: key); final BlockComponentContext blockComponentContext; final BlockComponentActionState blockComponentState; @@ -27,49 +20,26 @@ class BlockAddButton extends StatelessWidget { @override Widget build(BuildContext context) { return BlockActionButton( - svg: FlowySvgs.add_s, - richMessage: TextSpan( + svgName: 'editor/add', + richMessage: const TextSpan( 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(), + // todo: l10n. + text: 'Click to add below', ), ], ), onTap: () { - final isAltPressed = HardwareKeyboard.instance.isAltPressed; - final transaction = editorState.transaction; - - // If the current block is not an empty paragraph block, - // then insert a new block above/below the current block. + // if the current block is not a empty paragraph block, then insert a new block below the current block. final node = blockComponentContext.node; if (node.type != ParagraphBlockKeys.type || (node.delta?.isNotEmpty ?? true)) { - final path = isAltPressed ? node.path : node.path.next; - - transaction.insertNode(path, paragraphNode()); - transaction.afterSelection = Selection.collapsed( - Position(path: path), - ); + transaction.insertNode(node.path.next, paragraphNode()); + transaction.afterSelection = Selection.collapse(node.path.next, 0); } else { - transaction.afterSelection = Selection.collapsed( - Position(path: node.path), - ); + transaction.afterSelection = Selection.collapse(node.path, 0); } - // show the slash menu. editorState.apply(transaction).then( (_) => WidgetsBinding.instance.addPostFrameCallback( 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..4b9db59375 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,42 +1,38 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flutter/material.dart'; class BlockActionButton extends StatelessWidget { const BlockActionButton({ super.key, - required this.svg, + required this.svgName, required this.richMessage, required this.onTap, - this.showTooltip = true, - this.onPointerDown, }); - final FlowySvgData svg; - final bool showTooltip; + final String svgName; 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( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: FlowySvg( - svg, - size: const Size.square(18.0), - color: Theme.of(context).iconTheme.color, + return Align( + alignment: Alignment.center, + child: Tooltip( + preferBelow: false, + richMessage: richMessage, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: IgnoreParentGestureWidget( + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.deferToChild, + child: svgWidget( + svgName, + 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..40ae6feb00 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: 8.0), BlockOptionButton( blockComponentContext: blockComponentContext, blockComponentState: blockComponentState, actions: actions, editorState: editorState, - blockComponentBuilder: blockComponentBuilder, ), - const HSpace(5.0), + const SizedBox(width: 6.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..4a33a72653 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,130 @@ -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/workspace/application/settings/appearance/appearance_cubit.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/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 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; -import 'drag_to_reorder/draggable_option_button.dart'; - -class BlockOptionButton extends StatefulWidget { +class BlockOptionButton extends StatelessWidget { const BlockOptionButton({ - super.key, + Key? key, required this.blockComponentContext, required this.blockComponentState, required this.actions, required this.editorState, - required this.blockComponentBuilder, - }); + }) : super(key: key); 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); - case OptionAction.depth: - return DepthOptionAction(editorState: widget.editorState); - case OptionAction.turnInto: - return TurnIntoOptionAction( - editorState: widget.editorState, - blockComponentBuilder: widget.blockComponentBuilder, - mutex: mutex, - ); + return AlignOptionAction(editorState: editorState); default: return OptionActionWrapper(e); } }).toList(); + + return PopoverActionList( + direction: PopoverDirection.leftWithCenterAligned, + actions: popoverActions, + onPopupBuilder: () => blockComponentState.alwaysShowActions = true, + onClosed: () { + editorState.selectionType = null; + editorState.selection = null; + blockComponentState.alwaysShowActions = false; + }, + 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( + svgName: 'editor/option', + 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, offset: 0); + 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: + 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/rust-lib/flowy-database2/src/services/snapshot/entities.rs b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/editor_action_wrapper.dart similarity index 100% rename from frontend/rust-lib/flowy-database2/src/services/snapshot/entities.rs rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/editor_action_wrapper.dart 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 deleted file mode 100644 index a04190f8af..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.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/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.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:universal_platform/universal_platform.dart'; - -/// The ... button shows on the top right corner of a block. -/// -/// Default actions are: -/// - delete -/// - duplicate -/// - insert above -/// - insert below -/// -/// Only works on mobile. -class MobileBlockActionButtons extends StatelessWidget { - const MobileBlockActionButtons({ - super.key, - this.extendActionWidgets = const [], - this.showThreeDots = true, - required this.node, - required this.editorState, - required this.child, - }); - - final Node node; - final EditorState editorState; - final List extendActionWidgets; - final Widget child; - final bool showThreeDots; - - @override - Widget build(BuildContext context) { - if (!UniversalPlatform.isMobile) { - return child; - } - - if (!showThreeDots) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _showBottomSheet(context), - child: child, - ); - } - - const padding = 10.0; - return Stack( - children: [ - child, - Positioned( - top: padding, - right: padding, - child: FlowyIconButton( - icon: const FlowySvg( - FlowySvgs.three_dots_s, - ), - width: 20.0, - onPressed: () => _showBottomSheet(context), - ), - ), - ], - ); - } - - void _showBottomSheet(BuildContext context) { - // close the keyboard - editorState.updateSelectionWithReason(null, extraInfo: {}); - - showMobileBottomSheet( - context, - showHeader: true, - showCloseButton: true, - showDragHandle: true, - title: LocaleKeys.document_plugins_action.tr(), - builder: (context) { - return BlockActionBottomSheet( - extendActionWidgets: extendActionWidgets, - onAction: (action) async { - context.pop(); - - final transaction = editorState.transaction; - switch (action) { - case BlockActionBottomSheetType.delete: - transaction.deleteNode(node); - break; - case BlockActionBottomSheetType.duplicate: - transaction.insertNode( - node.path.next, - node.deepCopy(), - ); - break; - case BlockActionBottomSheetType.insertAbove: - case BlockActionBottomSheetType.insertBelow: - final path = action == BlockActionBottomSheetType.insertAbove - ? node.path - : node.path.next; - transaction - ..insertNode( - path, - paragraphNode(), - ) - ..afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - break; - } - - if (transaction.operations.isNotEmpty) { - await editorState.apply(transaction); - } - }, - ); - }, - ); - } -} 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..1c23c0b2ba --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -0,0 +1,304 @@ +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' hide FlowySvg; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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'; + +enum OptionAction { + delete, + duplicate, + turnInto, + moveUp, + moveDown, + color, + divider, + align; + + String get assetName { + switch (this) { + case OptionAction.delete: + return 'editor/delete'; + case OptionAction.duplicate: + return 'editor/duplicate'; + case OptionAction.turnInto: + return 'editor/turn_into'; + case OptionAction.moveUp: + return 'editor/move_up'; + case OptionAction.moveDown: + return 'editor/move_down'; + case OptionAction.color: + return 'editor/color'; + case OptionAction.divider: + return 'editor/divider'; + case OptionAction.align: + return 'editor/align/center'; + } + } + + 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.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; + } + } + + String get assetName { + switch (this) { + case OptionAlignType.left: + return 'editor/align/left'; + case OptionAlignType.center: + return 'editor/align/center'; + case OptionAlignType.right: + return 'editor/align/right'; + } + } + + 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 DividerOptionAction extends CustomActionCell { + @override + Widget buildWithContext(BuildContext context) { + 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( + name: align.assetName, + 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( + name: 'editor/color_formatter', + 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?.toColor(); + + final colors = [ + // clear background color. + FlowyColorOption( + color: Colors.transparent, + name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(), + ), + ...FlowyTint.values.map( + (e) => FlowyColorOption( + color: e.color(context), + name: e.tintName(AppFlowyEditorLocalizations.current), + ), + ), + ]; + + return FlowyColorPicker( + colors: colors, + selected: selectedColor, + border: Border.all( + color: Theme.of(context).colorScheme.onBackground, + width: 1, + ), + onTap: (color, index) async { + final transaction = editorState.transaction; + final backgroundColor = + color == Colors.transparent ? null : color.toHex(); + transaction.updateNode(node, { + blockComponentBackgroundColor: backgroundColor, + }); + await editorState.apply(transaction); + + controller.close(); + parentController.close(); + }, + ); + }; +} + +class OptionActionWrapper extends ActionCell { + OptionActionWrapper(this.inner); + + final OptionAction inner; + + @override + Widget? leftIcon(Color iconColor) => FlowySvg(name: inner.assetName); + + @override + String get name => inner.description; +} + +class OptionAlignWrapper extends ActionCell { + OptionAlignWrapper(this.inner); + + final OptionAlignType inner; + + @override + Widget? leftIcon(Color iconColor) => FlowySvg(name: inner.assetName); + + @override + String get name => inner.description; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart new file mode 100644 index 0000000000..1bbce0b5ad --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart @@ -0,0 +1,168 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/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:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; +import 'package:flutter/material.dart'; + +class OptionActionList extends StatelessWidget { + const OptionActionList({ + Key? key, + required this.blockComponentContext, + required this.blockComponentState, + required this.actions, + required this.editorState, + }) : super(key: key); + + final BlockComponentContext blockComponentContext; + final BlockComponentActionState blockComponentState; + final List actions; + final EditorState editorState; + + @override + Widget build(BuildContext context) { + final popoverActions = actions.map((e) { + if (e == OptionAction.divider) { + return DividerOptionAction(); + } else if (e == OptionAction.color) { + return ColorOptionAction( + editorState: editorState, + ); + } else { + return OptionActionWrapper(e); + } + }).toList(); + + return PopoverActionList( + direction: PopoverDirection.leftWithCenterAligned, + actions: popoverActions, + onPopupBuilder: () => blockComponentState.alwaysShowActions = true, + onClosed: () { + editorState.selectionType = null; + editorState.selection = null; + blockComponentState.alwaysShowActions = false; + }, + onSelected: (action, controller) { + if (action is OptionActionWrapper) { + _onSelectAction(action.inner); + controller.close(); + } + }, + buildChild: (controller) => OptionActionButton( + onTap: () { + controller.show(); + + // update selection + _updateBlockSelection(); + }, + ), + ); + } + + void _updateBlockSelection() { + final startNode = blockComponentContext.node; + var endNode = startNode; + while (endNode.children.isNotEmpty) { + endNode = endNode.children.last; + } + + final start = Position(path: startNode.path, offset: 0); + final end = endNode.selectable?.end() ?? + Position( + path: endNode.path, + offset: endNode.delta?.length ?? 0, + ); + + 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: + throw UnimplementedError(); + } + editorState.apply(transaction); + } +} + +class BlockComponentActionButton extends StatelessWidget { + const BlockComponentActionButton({ + super.key, + required this.icon, + required this.onTap, + }); + + final bool isHovering = false; + final Widget icon; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.grab, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + onTapDown: (details) {}, + onTapUp: (details) {}, + child: icon, + ), + ); + } +} + +class OptionActionButton extends StatelessWidget { + const OptionActionButton({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: IgnoreParentGestureWidget( + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.deferToChild, + child: svgWidget( + 'editor/option', + size: const Size.square(24.0), + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ), + ); + } +} 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 deleted file mode 100644 index cceac56c0d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart +++ /dev/null @@ -1,197 +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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.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, - group: 4, - isActive: onlyShowInTextType, - builder: (context, editorState, highlightColor, _, tooltipBuilder) { - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - - bool isSatisfyCondition(bool Function(Object? value) test) { - return nodes.every( - (n) => test(n.attributes[blockComponentAlign]), - ); - } - - bool isHighlight = false; - FlowySvgData data = FlowySvgs.toolbar_align_left_s; - if (isSatisfyCondition((value) => value == leftAlignmentKey)) { - isHighlight = true; - data = FlowySvgs.toolbar_align_left_s; - } else if (isSatisfyCondition((value) => value == centerAlignmentKey)) { - isHighlight = true; - data = FlowySvgs.toolbar_align_center_s; - } else if (isSatisfyCondition((value) => value == rightAlignmentKey)) { - isHighlight = true; - data = FlowySvgs.toolbar_align_right_s; - } - - Widget 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, - }, - ), - ); - }, - ); - - if (tooltipBuilder != null) { - child = tooltipBuilder( - context, - kAlignToolbarItemId, - LocaleKeys.document_plugins_optionAction_align.tr(), - child, - ); - } - - return child; - }, -); - -class _AlignmentButtons extends StatefulWidget { - const _AlignmentButtons({ - required this.child, - required this.onAlignChanged, - }); - - final Widget child; - final Function(String align) onAlignChanged; - - @override - State<_AlignmentButtons> createState() => _AlignmentButtonsState(); -} - -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), - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 10), - decorationColor: Theme.of(context).colorScheme.onTertiary, - borderRadius: BorderRadius.circular(6.0), - popupBuilder: (_) { - keepEditorFocusNotifier.increase(); - return _AlignButtons(onAlignChanged: widget.onAlignChanged); - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - }, - child: FlowyButton( - useIntrinsicWidth: true, - text: widget.child, - hoverColor: Colors.grey.withValues(alpha: 0.3), - onTap: () => controller.show(), - ), - ); - } -} - -class _AlignButtons extends StatelessWidget { - const _AlignButtons({ - required this.onAlignChanged, - }); - - final Function(String align) onAlignChanged; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 28, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HSpace(4), - _AlignButton( - icon: FlowySvgs.toolbar_align_left_s, - tooltips: LocaleKeys.document_plugins_optionAction_left.tr(), - onTap: () => onAlignChanged(leftAlignmentKey), - ), - const _Divider(), - _AlignButton( - icon: FlowySvgs.toolbar_align_center_s, - tooltips: LocaleKeys.document_plugins_optionAction_center.tr(), - onTap: () => onAlignChanged(centerAlignmentKey), - ), - const _Divider(), - _AlignButton( - icon: FlowySvgs.toolbar_align_right_s, - tooltips: LocaleKeys.document_plugins_optionAction_right.tr(), - onTap: () => onAlignChanged(rightAlignmentKey), - ), - const HSpace(4), - ], - ), - ); - } -} - -class _AlignButton extends StatelessWidget { - const _AlignButton({ - required this.icon, - required this.tooltips, - required this.onTap, - }); - - final FlowySvgData icon; - final String tooltips; - final VoidCallback onTap; - - @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, - ), - ), - ); - } -} - -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/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 deleted file mode 100644 index bc3f5cffa1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart +++ /dev/null @@ -1,76 +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'; - -final List customTextAlignCommands = [ - customTextLeftAlignCommand, - customTextCenterAlignCommand, - customTextRightAlignCommand, -]; - -/// Windows / Linux : ctrl + shift + l -/// macOS : ctrl + shift + l -/// Allows the user to align text to the left -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( - key: 'Align text to the left', - command: 'ctrl+shift+l', - getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignLeft.tr, - handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey), -); - -/// Windows / Linux : ctrl + shift + c -/// macOS : ctrl + shift + c -/// Allows the user to align text to the center -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent( - key: 'Align text to the center', - command: 'ctrl+shift+c', - getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignCenter.tr, - handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey), -); - -/// Windows / Linux : ctrl + shift + r -/// macOS : ctrl + shift + r -/// Allows the user to align text to the right -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent customTextRightAlignCommand = CommandShortcutEvent( - key: 'Align text to the right', - command: 'ctrl+shift+r', - getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignRight.tr, - handler: (editorState) => _textAlignHandler(editorState, rightAlignmentKey), -); - -KeyEventResult _textAlignHandler(EditorState editorState, String align) { - final Selection? selection = editorState.selection; - - if (selection == null) { - return KeyEventResult.ignored; - } - - editorState.updateNode( - selection, - (node) => node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: align, - }, - ), - ); - - return KeyEventResult.handled; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart deleted file mode 100644 index 25bc82283d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flowy_infra/theme_extension.dart'; - -// DON'T MODIFY THIS KEY BECAUSE IT'S SAVED IN THE DATABASE! -// Used for the block component background color -const themeBackgroundColors = { - 'appflowy_them_color_tint1': FlowyTint.tint1, - 'appflowy_them_color_tint2': FlowyTint.tint2, - 'appflowy_them_color_tint3': FlowyTint.tint3, - 'appflowy_them_color_tint4': FlowyTint.tint4, - 'appflowy_them_color_tint5': FlowyTint.tint5, - 'appflowy_them_color_tint6': FlowyTint.tint6, - 'appflowy_them_color_tint7': FlowyTint.tint7, - 'appflowy_them_color_tint8': FlowyTint.tint8, - 'appflowy_them_color_tint9': FlowyTint.tint9, -}; 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/build_context_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/build_context_extension.dart index 06d51094c3..2e0e40dfb4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/build_context_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/build_context_extension.dart @@ -12,9 +12,4 @@ extension BuildContextExtension on BuildContext { box.hitTest(result, position: box.globalToLocal(offset)); return result.path.any((entry) => entry.target == box); } - - double get appBarHeight => - AppBarTheme.of(this).toolbarHeight ?? kToolbarHeight; - double get statusBarHeight => statusBarAndAppBarHeight - appBarHeight; - double get statusBarAndAppBarHeight => MediaQuery.of(this).padding.top; } 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..82a1b9016f 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,21 +1,30 @@ -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/startup/startup.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_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:dartz/dartz.dart' as dartz; +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:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; class BuiltInPageWidget extends StatefulWidget { const BuiltInPageWidget({ - super.key, + Key? key, required this.node, required this.editorState, required this.builder, - }); + }) : super(key: key); final Node node; final EditorState editorState; @@ -26,8 +35,7 @@ class BuiltInPageWidget extends StatefulWidget { } class _BuiltInPageWidgetState extends State { - late Future> future; - + late Future> future; final focusNode = FocusNode(); String get parentViewId => widget.node.attributes[DatabaseBlockKeys.parentID]; @@ -36,10 +44,14 @@ class _BuiltInPageWidgetState extends State { @override void initState() { super.initState(); - future = ViewBackendService().getChildView( - parentViewId: parentViewId, - childViewId: childViewId, - ); + future = ViewBackendService() + .getChildView( + parentViewId: parentViewId, + childViewId: childViewId, + ) + .then( + (value) => value.swap(), + ); } @override @@ -50,21 +62,24 @@ class _BuiltInPageWidgetState extends State { @override Widget build(BuildContext context) { - return FutureBuilder>( + return FutureBuilder>( builder: (context, snapshot) { - final page = snapshot.data?.toNullable(); + final page = snapshot.data?.toOption().toNullable(); 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 +89,21 @@ 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: 400, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + 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,16 +111,92 @@ 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: svgWidget( + 'common/information', + color: Theme.of(context).iconTheme.color, + ), + ), + // 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), + icon: svgWidget( + 'common/settings', + color: Theme.of(context).iconTheme.color, + ), + onPressed: () => controller.show(), + ), + onSelected: (action, controller) async { + switch (action.inner) { + case _ActionType.viewDatabase: + getIt().latestOpenView = viewPB; + + getIt().setPlugin(viewPB.plugin()); + break; + case _ActionType.delete: + final transaction = widget.editorState.transaction; + transaction.deleteNode(widget.node); + widget.editorState.apply(transaction); + break; + } + controller.close(); + }, + ) + ], ); } Future _deletePage() async { final transaction = widget.editorState.transaction; transaction.deleteNode(widget.node); - await widget.editorState.apply(transaction); + widget.editorState.apply(transaction); + } +} + +enum _ActionType { + viewDatabase, + delete, +} + +class _ActionWrapper extends ActionCell { + final _ActionType inner; + + _ActionWrapper(this.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/rust-lib/flowy-document/tests/assets/text/divider.txt b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/color_extension.dart similarity index 100% rename from frontend/rust-lib/flowy-document/tests/assets/text/divider.txt rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/color_extension.dart 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..19fca1d08b 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,222 +1,58 @@ -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/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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({ super.key, required this.emoji, required this.onSubmitted, - this.emojiPickerSize = const Size(360, 380), + this.emojiPickerSize = const Size(300, 250), this.emojiSize = 18.0, - this.defaultIcon, - this.offset, - 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(Emoji 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, - offset: offset, - direction: direction, - title: title, - showBorder: showBorder, - enable: enable, - buttonSize: buttonSize, - tabs: tabs, - documentId: documentId, - ); - } - - 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, + triggerActions: PopoverTriggerFlags.click, 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); - }, - ), + popupBuilder: (context) => _buildEmojiPicker(), + child: FlowyTextButton( + emoji, + overflow: TextOverflow.visible, + fontSize: emojiSize, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 35.0), + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.center, + onPressed: () { + popoverController.show(); + }, ), - 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, - ), + ); + } + + Widget _buildEmojiPicker() { + return Container( + width: emojiPickerSize.width, + height: emojiPickerSize.height, + padding: const EdgeInsets.all(4.0), + child: EmojiSelectionMenu( + onSubmitted: (emoji) => onSubmitted(emoji, popoverController), + onExit: () {}, ), ); } } - -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, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/font_colors.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/font_colors.dart deleted file mode 100644 index e386be709a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/font_colors.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; - -class EditorFontColors { - static final lightColors = [ - const Color(0x00FFFFFF), - const Color(0xFFE8E0FF), - const Color(0xFFFFE6FD), - const Color(0xFFFFDAE6), - const Color(0xFFFFEFE3), - const Color(0xFFF5FFDC), - const Color(0xFFDDFFD6), - const Color(0xFFDEFFF1), - const Color(0xFFE1FBFF), - const Color(0xFFFFADAD), - const Color(0xFFFFE088), - const Color(0xFFA7DF4A), - const Color(0xFFD4C0FF), - const Color(0xFFFDB2FE), - const Color(0xFFFFD18B), - const Color(0xFF65E7F0), - const Color(0xFF71E6B4), - const Color(0xFF80F1FF), - ]; - - static final darkColors = [ - const Color(0x00FFFFFF), - const Color(0xFF8B80AD), - const Color(0xFF987195), - const Color(0xFF906D78), - const Color(0xFFA68B77), - const Color(0xFF88936D), - const Color(0xFF72936B), - const Color(0xFF6B9483), - const Color(0xFF658B90), - const Color(0xFF95405A), - const Color(0xFFA6784D), - const Color(0xFF6E9234), - const Color(0xFF6455A2), - const Color(0xFF924F83), - const Color(0xFFA48F34), - const Color(0xFF29A3AC), - const Color(0xFF2E9F84), - const Color(0xFF405EA6), - ]; - - // if the input color doesn't exist in the list, return the input color itself. - static Color? fromBuiltInColors(BuildContext context, Color? color) { - if (color == null) { - return null; - } - - final brightness = Theme.of(context).brightness; - - // if the dark mode color using light mode, return it's corresponding light color. Same for light mode. - if (brightness == Brightness.light) { - if (darkColors.contains(color)) { - return lightColors[darkColors.indexOf(color)]; - } - } else { - if (lightColors.contains(color)) { - return darkColors[lightColors.indexOf(color)]; - } - } - return color; - } -} 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 deleted file mode 100644 index 8548b9354c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart +++ /dev/null @@ -1,137 +0,0 @@ -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 - !! - -/// format '=' + '>' into an ⇒ -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -final CharacterShortcutEvent customFormatGreaterEqual = CharacterShortcutEvent( - key: 'format = + > into ⇒', - character: _greater, - handler: (editorState) async => _handleDoubleCharacterReplacement( - editorState: editorState, - character: _greater, - replacement: _equalGreater, - 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, - required String character, - required String replacement, - String? prefixCharacter, -}) async { - assert(character.length == 1); - - final selection = editorState.selection; - if (selection == null) { - return false; - } - - if (!selection.isCollapsed) { - await editorState.deleteSelection(selection); - } - - final node = editorState.getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || - delta == null || - delta.isEmpty || - node.type == CodeBlockKeys.type) { - return false; - } - - if (selection.end.offset > 0) { - final plain = delta.toPlainText(); - - final expectedPrevious = - prefixCharacter?.isEmpty ?? true ? character : prefixCharacter; - - final previousCharacter = plain[selection.end.offset - 1]; - if (previousCharacter != expectedPrevious) { - 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, - replacement, - ); - - await editorState.apply(replace); - - return true; - } - - return false; -} 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..debf00f4e1 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,10 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/domain/database_view_service.dart'; +import 'package:appflowy/plugins/database_view/application/database_view_service.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'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -14,7 +13,6 @@ extension InsertDatabase on EditorState { if (selection == null || !selection.isCollapsed) { return; } - final node = getNodeAtPath(selection.end.path); if (node == null) { return; @@ -36,7 +34,6 @@ extension InsertDatabase on EditorState { Future insertReferencePage( ViewPB childView, - ViewLayoutPB viewType, ) async { final selection = this.selection; if (selection == null || !selection.isCollapsed) { @@ -53,54 +50,24 @@ extension InsertDatabase on EditorState { ); } - final Transaction transaction = viewType == ViewLayoutPB.Document - ? await _insertDocumentReference(childView, selection, node) - : await _insertDatabaseReference(childView, selection.end.path); - - await apply(transaction); - } - - Future _insertDocumentReference( - ViewPB view, - Selection selection, - Node node, - ) async { - return transaction - ..replaceText( - node, - selection.end.offset, - 0, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: view.id, - blockId: null, - ), - ); - } - - Future _insertDatabaseReference( - ViewPB view, - List path, - ) async { // get the database id that the view is associated with - final databaseId = await DatabaseViewBackendService(viewId: view.id) + final databaseId = await DatabaseViewBackendService(viewId: childView.id) .getDatabaseId() - .then((value) => value.toNullable()); + .then((value) => value.swap().toOption().toNullable()); if (databaseId == null) { throw StateError( - 'The database associated with ${view.id} could not be found while attempting to create a referenced ${view.layout.name}.', + 'The database associated with ${childView.id} could not be found while attempting to create a referenced ${childView.layout.name}.', ); } - final prefix = _referencedDatabasePrefix(view.layout); + final prefix = _referencedDatabasePrefix(childView.layout); final ref = await ViewBackendService.createDatabaseLinkedView( - parentViewId: view.id, - name: "$prefix ${view.nameOrDefault}", - layoutType: view.layout, + parentViewId: childView.id, + name: "$prefix ${childView.name}", + layoutType: childView.layout, databaseId: databaseId, - ).then((value) => value.toNullable()); + ).then((value) => value.swap().toOption().toNullable()); if (ref == null) { throw FlowyError( @@ -109,17 +76,18 @@ extension InsertDatabase on EditorState { ); } - return transaction - ..insertNode( - path, - Node( - type: _convertPageType(view), - attributes: { - DatabaseBlockKeys.parentID: view.id, - DatabaseBlockKeys.viewID: ref.id, - }, - ), - ); + final transaction = this.transaction; + transaction.insertNode( + selection.end.path, + Node( + type: _convertPageType(childView), + attributes: { + DatabaseBlockKeys.parentID: childView.id, + DatabaseBlockKeys.viewID: ref.id, + }, + ), + ); + await apply(transaction); } String _referencedDatabasePrefix(ViewLayoutPB layout) { 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..902a551f65 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,74 +1,274 @@ -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'; -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.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_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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/error_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; -InlineActionsMenuService? _actionsMenuService; - -Future showLinkToPageMenu( +void showLinkToPageMenu( + OverlayState container, EditorState editorState, - SelectionMenuService menuService, { - ViewLayoutPB? pageType, - bool? insertPage, -}) async { - keepEditorFocusNotifier.increase(); - + SelectionMenuService menuService, + ViewLayoutPB pageType, +) { menuService.dismiss(); - _actionsMenuService?.dismiss(); - final rootContext = editorState.document.root.context; - if (rootContext == null) { - return; - } + final alignment = menuService.alignment; + final offset = menuService.offset; + final top = alignment == Alignment.bottomLeft ? offset.dy : null; + final bottom = alignment == Alignment.topLeft ? offset.dy : null; - final service = InlineActionsService( - context: rootContext, - handlers: [ - InlinePageReferenceService( - currentViewId: '', - viewLayout: pageType, - customTitle: titleFromPageType(pageType), - insertPage: insertPage ?? pageType != ViewLayoutPB.Document, - limitResults: 15, + keepEditorFocusNotifier.value += 1; + late OverlayEntry linkToPageMenuEntry; + linkToPageMenuEntry = FullScreenOverlayEntry( + top: top, + bottom: bottom, + left: offset.dx, + dismissCallback: () => keepEditorFocusNotifier.value -= 1, + builder: (context) => Material( + color: Colors.transparent, + child: LinkToPageMenu( + editorState: editorState, + layoutType: pageType, + hintText: pageType.toHintText(), + onSelected: (appPB, viewPB) async { + try { + await editorState.insertReferencePage(viewPB); + linkToPageMenuEntry.remove(); + } on FlowyError catch (e) { + Dialogs.show( + FlowyErrorPage.message( + e.msg, + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + context, + ); + } + }, ), - ], - ); + ), + ).build(); + container.insert(linkToPageMenuEntry); +} - final List initialResults = []; - for (final handler in service.handlers) { - final group = await handler.search(null); +class LinkToPageMenu extends StatefulWidget { + const LinkToPageMenu({ + super.key, + required this.editorState, + required this.layoutType, + required this.hintText, + required this.onSelected, + }); - if (group.results.isNotEmpty) { - initialResults.add(group); + final EditorState editorState; + final ViewLayoutPB layoutType; + final String hintText; + final void Function(ViewPB view, ViewPB childView) onSelected; + + @override + State createState() => _LinkToPageMenuState(); +} + +class _LinkToPageMenuState extends State { + final _focusNode = FocusNode(debugLabel: 'reference_list_widget'); + EditorStyle get style => widget.editorState.editorStyle; + int _selectedIndex = 0; + int _totalItems = 0; + Future)>>? _availableLayout; + final Map _items = {}; + + Future)>> fetchItems() async { + final items = await ViewBackendService().fetchViews(widget.layoutType); + + int index = 0; + for (final (app, children) in items) { + for (final view in children) { + _items.putIfAbsent(index, () => (app, view)); + index += 1; + } } + + _totalItems = _items.length; + return items; } - if (rootContext.mounted) { - _actionsMenuService = InlineActionsMenu( - context: rootContext, - editorState: editorState, - service: service, - initialResults: initialResults, - style: Theme.of(editorState.document.root.context!).brightness == - Brightness.light - ? const InlineActionsMenuStyle.light() - : const InlineActionsMenuStyle.dark(), - startCharAmount: 0, - ); + @override + void initState() { + _availableLayout = fetchItems(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + super.initState(); + } - await _actionsMenuService?.show(); + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Focus( + focusNode: _focusNode, + onKey: _onKey, + child: Container( + width: 300, + padding: const EdgeInsets.fromLTRB(10, 6, 10, 6), + decoration: BoxDecoration( + color: theme.cardColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: _buildListWidget( + context, + _selectedIndex, + _availableLayout, + ), + ), + ); + } + + KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + if (event is! RawKeyDownEvent || + _availableLayout == null || + _items.isEmpty) { + return KeyEventResult.ignored; + } + + final acceptedKeys = [ + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.tab, + LogicalKeyboardKey.enter + ]; + + if (!acceptedKeys.contains(event.logicalKey)) { + return KeyEventResult.handled; + } + + var newSelectedIndex = _selectedIndex; + if (event.logicalKey == LogicalKeyboardKey.arrowDown && + newSelectedIndex != _totalItems - 1) { + newSelectedIndex += 1; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp && + newSelectedIndex != 0) { + newSelectedIndex -= 1; + } else if (event.logicalKey == LogicalKeyboardKey.tab) { + newSelectedIndex += 1; + newSelectedIndex %= _totalItems; + } else if (event.logicalKey == LogicalKeyboardKey.enter) { + widget.onSelected( + _items[_selectedIndex]!.$1, + _items[_selectedIndex]!.$2, + ); + } + + setState(() { + _selectedIndex = newSelectedIndex; + }); + + return KeyEventResult.handled; + } + + Widget _buildListWidget( + BuildContext context, + int selectedIndex, + Future)>>? items, + ) { + int index = 0; + return FutureBuilder)>>( + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + final views = snapshot.data; + final List children = [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: FlowyText.regular( + widget.hintText, + fontSize: 10, + color: Colors.grey, + ), + ), + ]; + + if (views != null && views.isNotEmpty) { + for (final (view, viewChildren) in views) { + if (viewChildren.isNotEmpty) { + children.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: FlowyText.regular( + view.name, + ), + ), + ); + + for (final value in viewChildren) { + children.add( + FlowyButton( + isSelected: index == _selectedIndex, + leftIcon: svgWidget( + value.iconName, + color: Theme.of(context).iconTheme.color, + ), + text: FlowyText.regular(value.name), + onTap: () => widget.onSelected(view, value), + ), + ); + + index += 1; + } + } + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: items, + ); } } -String titleFromPageType(ViewLayoutPB? layout) => switch (layout) { - ViewLayoutPB.Grid => LocaleKeys.inlineActions_gridReference.tr(), - ViewLayoutPB.Document => LocaleKeys.inlineActions_docReference.tr(), - ViewLayoutPB.Board => LocaleKeys.inlineActions_boardReference.tr(), - ViewLayoutPB.Calendar => LocaleKeys.inlineActions_calReference.tr(), - _ => LocaleKeys.inlineActions_pageReference.tr(), - }; +extension on ViewLayoutPB { + String toHintText() { + switch (this) { + case ViewLayoutPB.Grid: + return LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr(); + + case ViewLayoutPB.Board: + return LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr(); + + case ViewLayoutPB.Calendar: + return LocaleKeys.document_slashMenu_calendar_selectACalendarToLinkTo + .tr(); + + default: + throw Exception('Unknown layout type'); + } + } +} 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 deleted file mode 100644 index 007e4ea298..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart'; -import 'package:appflowy/plugins/inline_actions/handlers/child_page.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 = '+'; - -CharacterShortcutEvent pageReferenceShortcutBrackets( - BuildContext context, - String viewId, - InlineActionsMenuStyle style, -) => - CharacterShortcutEvent( - key: 'show the inline page reference menu by [', - character: _bracketChar, - handler: (editorState) => inlinePageReferenceCommandHandler( - _bracketChar, - context, - viewId, - editorState, - style, - previousChar: _bracketChar, - ), - ); - -CharacterShortcutEvent pageReferenceShortcutPlusSign( - BuildContext context, - String viewId, - InlineActionsMenuStyle style, -) => - CharacterShortcutEvent( - key: 'show the inline page reference menu by +', - character: _plusChar, - handler: (editorState) => inlinePageReferenceCommandHandler( - _plusChar, - context, - viewId, - editorState, - style, - ), - ); - -InlineActionsMenuService? selectionMenuService; - -Future inlinePageReferenceCommandHandler( - String character, - BuildContext context, - String currentViewId, - EditorState editorState, - InlineActionsMenuStyle style, { - String? previousChar, -}) async { - final selection = editorState.selection; - if (selection == null) { - return false; - } - - if (!selection.isCollapsed) { - await editorState.deleteSelection(selection); - } - - // Check for previous character - if (previousChar != null) { - final node = editorState.getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null || delta.isEmpty) { - return false; - } - - if (selection.end.offset > 0) { - final plain = delta.toPlainText(); - - final previousCharacter = plain[selection.end.offset - 1]; - if (previousCharacter != _bracketChar) { - return false; - } - } - } - - if (!context.mounted) { - return false; - } - - final service = InlineActionsService( - context: context, - handlers: [ - if (FeatureFlag.inlineSubPageMention.isOn) - InlineChildPageService(currentViewId: currentViewId), - InlinePageReferenceService( - currentViewId: currentViewId, - limitResults: 10, - ), - ], - ); - - await editorState.insertTextAtPosition(character, position: selection.start); - - final List initialResults = []; - for (final handler in service.handlers) { - final group = await handler.search(null); - - if (group.results.isNotEmpty) { - initialResults.add(group); - } - } - - 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(); - - await selectionMenuService?.show(); - - // enable the keyboard service - editorState.service.keyboardService?.enable(); - } - - 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..c87bc3ee90 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,7 +1,4 @@ -import 'dart:math'; - -// ignore: implementation_imports -import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -11,34 +8,24 @@ class SelectableItemListMenu extends StatelessWidget { required this.items, 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 - /// amount of space required to show all the items. - /// - final bool shrinkWrap; @override Widget build(BuildContext context) { - return ScrollablePositionedList.builder( - physics: const ClampingScrollPhysics(), - shrinkWrap: shrinkWrap, + return ListView.builder( + itemBuilder: (context, index) { + final item = items[index]; + return SelectableItem( + isSelected: index == selectedIndex, + item: item, + onTap: () => onSelected(index), + ); + }, itemCount: items.length, - itemScrollController: controller, - initialScrollIndex: max(0, selectedIndex), - itemBuilder: (context, index) => SelectableItem( - isSelected: index == selectedIndex, - item: items[index], - onTap: () => onSelected(index), - ), ); } } @@ -60,11 +47,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(name: 'grid/checkmark') : 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..2a08f181cc 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,38 +1,28 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/image.dart'; import 'package:flutter/material.dart'; class SelectableSvgWidget extends StatelessWidget { const SelectableSvgWidget({ super.key, - required this.data, + required this.name, required this.isSelected, required this.style, - this.size, - this.padding, }); - final FlowySvgData data; + final String name; final bool isSelected; final SelectionMenuStyle style; - final Size? size; - final EdgeInsets? padding; @override Widget build(BuildContext context) { - final child = FlowySvg( - data, - size: size ?? const Size.square(16.0), + return svgWidget( + name, + 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 deleted file mode 100644 index 77245a9f95..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart +++ /dev/null @@ -1,66 +0,0 @@ -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) { - return false; - } - final nodes = editorState.getNodesInSelection(selection); - return nodes.every((element) { - if (_isTableType(element.type)) { - return false; - } - var parent = element.parent; - while (parent != null) { - if (_isTableType(parent.type)) { - return false; - } - parent = parent.parent; - } - return true; - }); -} - -bool onlyShowInSingleTextTypeSelectionAndExcludeTable( - EditorState editorState, -) { - 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 deleted file mode 100644 index 735b6b15df..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart +++ /dev/null @@ -1,31 +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 MenuBlockButton extends StatelessWidget { - const MenuBlockButton({ - super.key, - required this.tooltip, - required this.iconData, - this.onTap, - }); - - final VoidCallback? onTap; - final String tooltip; - final FlowySvgData iconData; - - @override - Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - onTap: onTap, - text: FlowyTooltip( - message: tooltip, - child: FlowySvg( - iconData, - size: const Size.square(16), - ), - ), - ); - } -} 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 deleted file mode 100644 index 23b73e75a9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class BulletedListIcon extends StatelessWidget { - const BulletedListIcon({ - super.key, - required this.node, - }); - - final Node node; - - static final bulletedListIcons = [ - FlowySvgs.bulleted_list_icon_1_s, - FlowySvgs.bulleted_list_icon_2_s, - FlowySvgs.bulleted_list_icon_3_s, - ]; - - int get level { - var level = 0; - var parent = node.parent; - while (parent != null) { - if (parent.type == BulletedListBlockKeys.type) { - level++; - } - parent = parent.parent; - } - return level; - } - - @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), - ); - return Container( - width: size, - height: size, - margin: const EdgeInsets.only(right: 8.0), - alignment: Alignment.center, - 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..8eb97c6d41 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,18 +1,7 @@ -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; -import 'package:easy_localization/easy_localization.dart' hide TextDirection; -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'; // defining the keys of the callout block's attributes for easy access @@ -35,23 +24,18 @@ 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 +// creating a new callout node Node calloutNode({ Delta? delta, - EmojiIconData? emoji, - Color? defaultColor, + String emoji = '📌', + String backgroundColor = '#F0F0F0', }) { - final defaultEmoji = emoji ?? EmojiIconData.emoji('📌'); final attributes = { CalloutBlockKeys.delta: (delta ?? Delta()).toJson(), - CalloutBlockKeys.icon: defaultEmoji.emoji, - CalloutBlockKeys.iconType: defaultEmoji.type, - CalloutBlockKeys.backgroundColor: defaultColor?.toHex(), + CalloutBlockKeys.icon: emoji, + CalloutBlockKeys.backgroundColor: backgroundColor, }; return Node( type: CalloutBlockKeys.type, @@ -59,13 +43,12 @@ Node calloutNode({ ); } -// defining the callout block menu item in selection menu +// defining the callout block menu item for selection SelectionMenuItem calloutItem = SelectionMenuItem.node( - getName: LocaleKeys.document_plugins_callout.tr, + name: 'Callout', iconData: Icons.note, - keywords: [CalloutBlockKeys.type], - nodeBuilder: (editorState, context) => - calloutNode(defaultColor: Colors.transparent), + keywords: ['callout'], + nodeBuilder: (editorState) => calloutNode(), replace: (_, node) => node.delta?.isEmpty ?? false, updateSelection: (_, path, __, ___) { return Selection.single(path: path, startOffset: 0); @@ -75,13 +58,11 @@ SelectionMenuItem calloutItem = SelectionMenuItem.node( // building the callout block widget class CalloutBlockComponentBuilder extends BlockComponentBuilder { CalloutBlockComponentBuilder({ - super.configuration, - required this.defaultColor, - required this.inlinePadding, + this.configuration = const BlockComponentConfiguration(), }); - final Color defaultColor; - final EdgeInsets Function(Node node) inlinePadding; + @override + final BlockComponentConfiguration configuration; @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -89,23 +70,22 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder { return CalloutBlockComponentWidget( 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 && + node.attributes[CalloutBlockKeys.backgroundColor] is String; } // the main widget for rendering the callout block @@ -115,15 +95,9 @@ 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() => _CalloutBlockComponentWidgetState(); @@ -131,14 +105,7 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget { class _CalloutBlockComponentWidgetState extends State - with - SelectableMixin, - DefaultSelectableMixin, - BlockComponentConfigurable, - BlockComponentTextDirectionMixin, - BlockComponentAlignMixin, - BlockComponentBackgroundColorMixin, - NestedBlockComponentStatefulWidgetMixin { + with SelectableMixin, DefaultSelectable, BlockComponentConfigurable { // the key used to forward focus to the richtext child @override final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); @@ -147,180 +114,69 @@ class _CalloutBlockComponentWidgetState @override GlobalKey> get containerKey => widget.node.key; - @override - GlobalKey> blockComponentKey = GlobalKey( - debugLabel: CalloutBlockKeys.type, - ); - @override BlockComponentConfiguration get configuration => widget.configuration; @override Node get node => widget.node; - @override + // get the background color of the note block from the node's attributes Color get backgroundColor { - final color = super.backgroundColor; - if (color == Colors.transparent) { - return AFThemeExtension.of(context).calloutBGColor; + final colorString = + node.attributes[CalloutBlockKeys.backgroundColor] as String?; + if (colorString == null) { + return Colors.transparent; } - return color; + return colorString.toColor(); } // get the emoji of the note block from the node's attributes or default to '📌' - EmojiIconData 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; - } + String get emoji => node.attributes[CalloutBlockKeys.icon] ?? '📌'; - @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, - ); - } - - return child; - } - - @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; - } + // get access to the editor state via provider + late final editorState = Provider.of(context, listen: false); // build the callout block widget @override - Widget buildComponent( - BuildContext context, { - bool withBackgroundColor = true, - }) { - final textDirection = calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ); - final (emojiSize, emojiButtonSize) = calculateEmojiSize(); - final documentId = context.read()?.documentId; + Widget build(BuildContext context) { 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, - 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(); - }, - ), - if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0), - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: buildCalloutBlockComponent(context, textDirection), + Padding( + padding: const EdgeInsets.all(2.0), + child: EmojiPickerButton( + key: ValueKey( + emoji.toString(), + ), // force to refresh the popover state + emoji: emoji, + onSubmitted: (emoji, controller) { + setEmoji(emoji.emoji); + controller.close(); + }, ), ), - const HSpace(8.0), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: buildCalloutBlockComponent(context), + ), + ), + const VSpace(10), ], ), ); - 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 = 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) { + if (widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: widget.node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } @@ -329,50 +185,34 @@ class _CalloutBlockComponentWidgetState } // build the richtext child - Widget buildCalloutBlockComponent( - 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), + Widget buildCalloutBlockComponent(BuildContext context) { + return Padding( + padding: padding, + child: FlowyRichText( + key: forwardKey, + node: widget.node, + editorState: editorState, + placeholderText: placeholderText, + textSpanDecorator: (textSpan) => textSpan.updateTextStyle( + textStyle, + ), + placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( + placeholderTextStyle, + ), ), - 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), + ..afterSelection = Selection.collapse( + node.path, + 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 deleted file mode 100644 index 842f3f59fd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/services.dart'; - -/// Pressing Enter in a callout block will insert a newline (\n) within the callout, -/// while pressing Shift+Enter in a callout will insert a new paragraph next to the callout. -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -final CharacterShortcutEvent insertNewLineInCalloutBlock = - CharacterShortcutEvent( - key: 'insert a new line in callout 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 != CalloutBlockKeys.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) { - // 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; -}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart new file mode 100644 index 0000000000..c4731499ea --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart @@ -0,0 +1,368 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.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:highlight/highlight.dart' as highlight; +import 'package:highlight/languages/all.dart'; +import 'package:provider/provider.dart'; + +class CodeBlockKeys { + const CodeBlockKeys._(); + + static const String type = 'code'; + + /// The content of a code block. + /// + /// The value is a String. + static const String delta = 'delta'; + + /// The language of a code block. + /// + /// The value is a String. + static const String language = 'language'; +} + +Node codeBlockNode({ + Delta? delta, + String? language, +}) { + final attributes = { + CodeBlockKeys.delta: (delta ?? Delta()).toJson(), + CodeBlockKeys.language: language, + }; + return Node( + type: CodeBlockKeys.type, + attributes: attributes, + ); +} + +// defining the callout block menu item for selection +SelectionMenuItem codeBlockItem = SelectionMenuItem.node( + name: 'Code Block', + iconData: Icons.abc, + keywords: ['code', 'codeblock'], + nodeBuilder: (editorState) => codeBlockNode(), + replace: (_, node) => node.delta?.isEmpty ?? false, +); + +class CodeBlockComponentBuilder extends BlockComponentBuilder { + CodeBlockComponentBuilder({ + this.configuration = const BlockComponentConfiguration(), + this.padding = const EdgeInsets.all(0), + }); + + @override + final BlockComponentConfiguration configuration; + + final EdgeInsets padding; + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return CodeBlockComponentWidget( + key: node.key, + node: node, + configuration: configuration, + padding: padding, + showActions: showActions(node), + actionBuilder: (context, state) => actionBuilder( + blockComponentContext, + state, + ), + ); + } + + @override + bool validate(Node node) => node.delta != null; +} + +class CodeBlockComponentWidget extends BlockComponentStatefulWidget { + const CodeBlockComponentWidget({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + this.padding = const EdgeInsets.all(0), + }); + + final EdgeInsets padding; + + @override + State createState() => + _CodeBlockComponentWidgetState(); +} + +class _CodeBlockComponentWidgetState extends State + with SelectableMixin, DefaultSelectable, BlockComponentConfigurable { + // the key used to forward focus to the richtext child + @override + final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); + + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + GlobalKey> get containerKey => node.key; + + @override + Node get node => widget.node; + + final popoverController = PopoverController(); + + final supportedLanguages = [ + 'Assembly', + 'Bash', + 'BASIC', + 'C', + 'C#', + 'C++', + 'Clojure', + 'CSS', + 'Dart', + 'Docker', + 'Elixir', + 'Elm', + 'Erlang', + 'Fortran', + 'Go', + 'GraphQL', + 'Haskell', + 'HTML', + 'Java', + 'JavaScript', + 'JSON', + 'Kotlin', + 'LaTeX', + 'Lisp', + 'Lua', + 'Markdown', + 'MATLAB', + 'Objective-C', + 'OCaml', + 'Perl', + 'PHP', + 'PowerShell', + 'Python', + 'R', + 'Ruby', + 'Rust', + 'Scala', + 'Shell', + 'SQL', + 'Swift', + 'TypeScript', + 'Visual Basic', + 'XML', + 'YAML', + ]; + late final languages = supportedLanguages + .map((e) => e.toLowerCase()) + .toSet() + .intersection(allLanguages.keys.toSet()) + .toList(); + + late final editorState = context.read(); + + String? get language => node.attributes[CodeBlockKeys.language] as String?; + String? autoDetectLanguage; + + @override + Widget build(BuildContext context) { + Widget child = Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: Colors.grey.withOpacity(0.1), + ), + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildSwitchLanguageButton(context), + _buildCodeBlock(context), + ], + ), + ); + + if (widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: widget.node, + actionBuilder: widget.actionBuilder!, + child: child, + ); + } + + return child; + } + + Widget _buildCodeBlock(BuildContext context) { + final delta = node.delta ?? Delta(); + final content = delta.toPlainText(); + + final result = highlight.highlight.parse( + content, + language: language, + autoDetection: language == null, + ); + autoDetectLanguage = language ?? result.language; + + final codeNodes = result.nodes; + if (codeNodes == null) { + throw Exception('Code block parse error.'); + } + final codeTextSpans = _convert(codeNodes); + return Padding( + padding: widget.padding, + child: FlowyRichText( + key: forwardKey, + node: widget.node, + editorState: editorState, + placeholderText: placeholderText, + lineHeight: 1.5, + textSpanDecorator: (textSpan) => TextSpan( + style: textStyle, + children: codeTextSpans, + ), + placeholderTextSpanDecorator: (textSpan) => TextSpan( + style: textStyle, + ), + ), + ); + } + + Widget _buildSwitchLanguageButton(BuildContext context) { + const maxWidth = 100.0; + return AppFlowyPopover( + controller: popoverController, + child: Container( + width: maxWidth, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 4), + child: FlowyTextButton( + '${language?.capitalize() ?? 'auto'} ', + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 4.0, + ), + constraints: const BoxConstraints(maxWidth: maxWidth), + fontColor: Theme.of(context).colorScheme.onBackground, + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.start, + onPressed: () {}, + ), + ), + popupBuilder: (BuildContext context) { + return SelectableItemListMenu( + items: languages.map((e) => e.capitalize()).toList(), + selectedIndex: languages.indexOf(language ?? ''), + onSelected: (index) { + updateLanguage(languages[index]); + popoverController.close(); + }, + ); + }, + ); + } + + Future updateLanguage(String language) async { + final transaction = editorState.transaction + ..updateNode(node, { + CodeBlockKeys.language: language, + }) + ..afterSelection = Selection.collapse( + node.path, + node.delta?.length ?? 0, + ); + await editorState.apply(transaction); + } + + // Copy from flutter.highlight package. + // https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart + List _convert(List nodes) { + final List spans = []; + var currentSpans = spans; + final List> stack = []; + + void traverse(highlight.Node node) { + if (node.value != null) { + currentSpans.add( + node.className == null + ? TextSpan(text: node.value) + : TextSpan( + text: node.value, + style: _builtInCodeBlockTheme[node.className!], + ), + ); + } else if (node.children != null) { + final List tmp = []; + currentSpans.add( + TextSpan( + children: tmp, + style: _builtInCodeBlockTheme[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; + } +} + +const _builtInCodeBlockTheme = { + 'root': + TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)), + 'comment': TextStyle(color: Color(0xff007400)), + 'quote': TextStyle(color: Color(0xff007400)), + 'tag': TextStyle(color: Color(0xffaa0d91)), + 'attribute': TextStyle(color: Color(0xffaa0d91)), + 'keyword': TextStyle(color: Color(0xffaa0d91)), + 'selector-tag': TextStyle(color: Color(0xffaa0d91)), + 'literal': TextStyle(color: Color(0xffaa0d91)), + 'name': TextStyle(color: Color(0xffaa0d91)), + 'variable': TextStyle(color: Color(0xff3F6E74)), + 'template-variable': TextStyle(color: Color(0xff3F6E74)), + 'code': TextStyle(color: Color(0xffc41a16)), + 'string': TextStyle(color: Color(0xffc41a16)), + 'meta-string': TextStyle(color: Color(0xffc41a16)), + 'regexp': TextStyle(color: Color(0xff0E0EFF)), + 'link': TextStyle(color: Color(0xff0E0EFF)), + 'title': TextStyle(color: Color(0xff1c00cf)), + 'symbol': TextStyle(color: Color(0xff1c00cf)), + 'bullet': TextStyle(color: Color(0xff1c00cf)), + 'number': TextStyle(color: Color(0xff1c00cf)), + 'section': TextStyle(color: Color(0xff643820)), + 'meta': TextStyle(color: Color(0xff643820)), + 'type': TextStyle(color: Color(0xff5c2699)), + 'built_in': TextStyle(color: Color(0xff5c2699)), + 'builtin-name': TextStyle(color: Color(0xff5c2699)), + 'params': TextStyle(color: Color(0xff5c2699)), + 'attr': TextStyle(color: Color(0xff836C28)), + 'subst': TextStyle(color: Color(0xff000000)), + 'formula': TextStyle( + backgroundColor: Color(0xffeeeeee), + fontStyle: FontStyle.italic, + ), + 'addition': TextStyle(backgroundColor: Color(0xffbaeeba)), + 'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)), + 'selector-id': TextStyle(color: Color(0xff9b703f)), + 'selector-class': TextStyle(color: Color(0xff9b703f)), + 'doctag': TextStyle(fontWeight: FontWeight.bold), + 'strong': TextStyle(fontWeight: FontWeight.bold), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), +}; 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 deleted file mode 100644 index 645de3b2f8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart +++ /dev/null @@ -1,63 +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/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_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); - -class _CopyButton extends StatelessWidget { - const _CopyButton({required this.node}); - - final Node node; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4), - child: FlowyTooltip( - 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()), - ), - ); - - if (context.mounted) { - showToastNotification( - message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(), - ); - } - }, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - icon: FlowySvg( - FlowySvgs.copy_s, - color: AFThemeExtension.of(context).textColor, - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index c4c2e3e0ba..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart'; -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: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, - supportedLanguages, - onLanguageSelected, { - selectedLanguage, - onMenuClose, - onMenuOpen, -}) => - CodeBlockLanguageSelector( - editorState: editorState, - language: selectedLanguage, - supportedLanguages: supportedLanguages, - onLanguageSelected: onLanguageSelected, - onMenuClose: onMenuClose, - onMenuOpen: onMenuOpen, - ); - -class CodeBlockLanguageSelector extends StatefulWidget { - const CodeBlockLanguageSelector({ - super.key, - required this.editorState, - required this.supportedLanguages, - this.language, - required this.onLanguageSelected, - this.onMenuOpen, - this.onMenuClose, - }); - - final EditorState editorState; - final List supportedLanguages; - final String? language; - final void Function(String) onLanguageSelected; - final VoidCallback? onMenuOpen; - final VoidCallback? onMenuClose; - - @override - State createState() => - _CodeBlockLanguageSelectorState(); -} - -class _CodeBlockLanguageSelectorState extends State { - final controller = PopoverController(); - - @override - void dispose() { - controller.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget child = Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), - child: FlowyTextButton( - widget.language?.capitalize() ?? - LocaleKeys.document_codeBlock_language_auto.tr(), - constraints: const BoxConstraints(minWidth: 50), - fontColor: AFThemeExtension.of(context).onBackground, - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4), - fillColor: Colors.transparent, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - onPressed: () async { - if (UniversalPlatform.isMobile) { - final language = await context - .push(MobileCodeLanguagePickerScreen.routeName); - if (language != null) { - widget.onLanguageSelected(language); - } - } - }, - ), - ), - ], - ); - - if (UniversalPlatform.isDesktopOrWeb) { - child = AppFlowyPopover( - controller: controller, - direction: PopoverDirection.bottomWithLeftAligned, - onOpen: widget.onMenuOpen, - constraints: const BoxConstraints(maxHeight: 300, maxWidth: 200), - onClose: widget.onMenuClose, - popupBuilder: (_) => _LanguageSelectionPopover( - editorState: widget.editorState, - language: widget.language, - supportedLanguages: widget.supportedLanguages, - onLanguageSelected: (language) { - widget.onLanguageSelected(language); - controller.close(); - }, - ), - child: child, - ); - } - - return child; - } -} - -class _LanguageSelectionPopover extends StatefulWidget { - const _LanguageSelectionPopover({ - required this.editorState, - required this.language, - required this.supportedLanguages, - required this.onLanguageSelected, - }); - - final EditorState editorState; - final String? language; - final List supportedLanguages; - final void Function(String) onLanguageSelected; - - @override - State<_LanguageSelectionPopover> createState() => - _LanguageSelectionPopoverState(); -} - -class _LanguageSelectionPopoverState extends State<_LanguageSelectionPopover> { - final searchController = TextEditingController(); - final focusNode = FocusNode(); - late List filteredLanguages = - widget.supportedLanguages.map((e) => e.capitalize()).toList(); - late int selectedIndex = - widget.supportedLanguages.indexOf(widget.language?.toLowerCase() ?? ''); - final ItemScrollController languageListController = ItemScrollController(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - // This is a workaround because longer taps might break the - // focus, this might be an issue with the Flutter framework. - (_) => Future.delayed( - const Duration(milliseconds: 100), - () => focusNode.requestFocus(), - ), - ); - } - - @override - void dispose() { - focusNode.dispose(); - searchController.dispose(); - super.dispose(); - } - - @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, - ), - ), - ], - ), - ), - ); - } - - 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/code_block/code_block_shortcut_event.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_shortcut_event.dart new file mode 100644 index 0000000000..f8b59c0f67 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_shortcut_event.dart @@ -0,0 +1,266 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final List codeBlockCharacterEvents = [ + enterInCodeBlock, + ...ignoreKeysInCodeBlock, +]; + +final List codeBlockCommands = [ + insertNewParagraphNextToCodeBlockCommand, + tabToInsertSpacesInCodeBlockCommand, + tabToDeleteSpacesInCodeBlockCommand, + selectAllInCodeBlockCommand, +]; + +/// press the enter key in code block to insert a new line in it. +/// +/// - support +/// - desktop +/// - web +/// - mobile +/// +final CharacterShortcutEvent enterInCodeBlock = CharacterShortcutEvent( + key: 'press enter in code block', + character: '\n', + handler: _enterInCodeBlockCommandHandler, +); + +/// ignore ' ', '/', '_', '*' in code block. +/// +/// - support +/// - desktop +/// - web +/// - mobile +/// +final List ignoreKeysInCodeBlock = + [' ', '/', '_', '*', '~'] + .map( + (e) => CharacterShortcutEvent( + key: 'press enter in code block', + character: e, + handler: (editorState) => _ignoreKeysInCodeBlockCommandHandler( + editorState, + e, + ), + ), + ) + .toList(); + +/// shift + enter to insert a new node next to the code block. +/// +/// - support +/// - desktop +/// - web +/// +final CommandShortcutEvent insertNewParagraphNextToCodeBlockCommand = + CommandShortcutEvent( + key: 'insert a new paragraph next to the code block', + command: 'shift+enter', + handler: _insertNewParagraphNextToCodeBlockCommandHandler, +); + +/// tab to insert two spaces at the line start in code block. +/// +/// - support +/// - desktop +/// - web +final CommandShortcutEvent tabToInsertSpacesInCodeBlockCommand = + CommandShortcutEvent( + key: 'tab to insert two spaces at the line start in code block', + command: 'tab', + handler: _tabToInsertSpacesInCodeBlockCommandHandler, +); + +/// shift+tab to delete two spaces at the line start in code block if needed. +/// +/// - support +/// - desktop +/// - web +final CommandShortcutEvent tabToDeleteSpacesInCodeBlockCommand = + CommandShortcutEvent( + key: 'shift + tab to delete two spaces at the line start in code block', + command: 'shift+tab', + handler: _tabToDeleteSpacesInCodeBlockCommandHandler, +); + +/// CTRL+A to select all content inside a Code Block, if cursor is inside one. +/// +/// - support +/// - desktop +/// - web +final CommandShortcutEvent selectAllInCodeBlockCommand = CommandShortcutEvent( + key: 'ctrl + a to select all content inside a code block', + command: 'ctrl+a', + macOSCommand: 'meta+a', + handler: _selectAllInCodeBlockCommandHandler, +); + +CharacterShortcutEventHandler _enterInCodeBlockCommandHandler = + (editorState) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return false; + } + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null || node.type != CodeBlockKeys.type) { + return false; + } + final transaction = editorState.transaction + ..insertText( + node, + selection.end.offset, + '\n', + ); + await editorState.apply(transaction); + return true; +}; + +Future _ignoreKeysInCodeBlockCommandHandler( + EditorState editorState, + String key, +) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return false; + } + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null || node.type != CodeBlockKeys.type) { + return false; + } + await editorState.insertTextAtCurrentSelection(key); + return true; +} + +CommandShortcutEventHandler _insertNewParagraphNextToCodeBlockCommandHandler = + (editorState) { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return KeyEventResult.ignored; + } + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null || node.type != CodeBlockKeys.type) { + return KeyEventResult.ignored; + } + final sliced = delta.slice(selection.startIndex); + final transaction = editorState.transaction + ..deleteText( + // delete the text after the cursor in the code block + node, + selection.startIndex, + delta.length - selection.startIndex, + ) + ..insertNode( + // insert a new paragraph node with the sliced delta after the code block + selection.end.path.next, + paragraphNode( + attributes: { + 'delta': sliced.toJson(), + }, + ), + ) + ..afterSelection = Selection.collapse( + selection.end.path.next, + 0, + ); + editorState.apply(transaction); + return KeyEventResult.handled; +}; + +CommandShortcutEventHandler _tabToInsertSpacesInCodeBlockCommandHandler = + (editorState) { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return KeyEventResult.ignored; + } + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null || node.type != CodeBlockKeys.type) { + return KeyEventResult.ignored; + } + const spaces = ' '; + final lines = delta.toPlainText().split('\n'); + var index = 0; + for (final line in lines) { + if (index <= selection.endIndex && + selection.endIndex <= index + line.length) { + final transaction = editorState.transaction + ..insertText( + node, + index, + spaces, // two spaces + ) + ..afterSelection = Selection.collapse( + selection.end.path, + selection.endIndex + spaces.length, + ); + editorState.apply(transaction); + break; + } + index += line.length + 1; + } + return KeyEventResult.handled; +}; + +CommandShortcutEventHandler _tabToDeleteSpacesInCodeBlockCommandHandler = + (editorState) { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return KeyEventResult.ignored; + } + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null || node.type != CodeBlockKeys.type) { + return KeyEventResult.ignored; + } + const spaces = ' '; + final lines = delta.toPlainText().split('\n'); + var index = 0; + for (final line in lines) { + if (index <= selection.endIndex && + selection.endIndex <= index + line.length) { + if (line.startsWith(spaces)) { + final transaction = editorState.transaction + ..deleteText( + node, + index, + spaces.length, // two spaces + ) + ..afterSelection = Selection.collapse( + selection.end.path, + selection.endIndex - spaces.length, + ); + editorState.apply(transaction); + } + break; + } + index += line.length + 1; + } + return KeyEventResult.handled; +}; + +CommandShortcutEventHandler _selectAllInCodeBlockCommandHandler = + (editorState) { + final selection = editorState.selection; + if (selection == null || !selection.isSingle) { + return KeyEventResult.ignored; + } + + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null || node.type != CodeBlockKeys.type) { + return KeyEventResult.ignored; + } + + editorState.service.selectionService.updateSelection( + Selection.single( + path: node.path, + startOffset: 0, + endOffset: delta.length, + ), + ); + + return KeyEventResult.handled; +}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart deleted file mode 100644 index 8c4a239e20..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart +++ /dev/null @@ -1,42 +0,0 @@ -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/document/presentation/editor_plugins/base/string_extension.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:go_router/go_router.dart'; - -class MobileCodeLanguagePickerScreen extends StatelessWidget { - const MobileCodeLanguagePickerScreen({super.key}); - - static const routeName = '/code_language_picker'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: FlowyAppBar(titleText: LocaleKeys.titleBar_language.tr()), - body: SafeArea( - child: ListView.separated( - separatorBuilder: (_, __) => const Divider(), - itemCount: defaultCodeBlockSupportedLanguages.length, - itemBuilder: (context, index) { - final language = defaultCodeBlockSupportedLanguages[index]; - return SizedBox( - height: 48, - child: FlowyTextButton( - language.capitalize(), - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 4.0, - ), - onPressed: () => context.pop(language), - ), - ); - }, - ), - ), - ); - } -} 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 deleted file mode 100644 index cc496ff9d4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/context_menu/custom_context_menu.dart +++ /dev/null @@ -1,26 +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'; - -final List> customContextMenuItems = [ - [ - ContextMenuItem( - getName: LocaleKeys.document_plugins_contextMenu_copy.tr, - onPressed: (editorState) => customCopyCommand.execute(editorState), - ), - ContextMenuItem( - 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 deleted file mode 100644 index f108c7e26b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:flutter/foundation.dart'; -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( - 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, -); - -class ClipboardServiceData { - const ClipboardServiceData({ - this.plainText, - 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) { - item.add(Formats.plainText(plainText)); - } - if (html != null) { - item.add(Formats.htmlText(html)); - } - 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': - item.add(Formats.png(image.$2!)); - break; - case 'jpeg': - item.add(Formats.jpeg(image.$2!)); - break; - case 'gif': - item.add(Formats.gif(image.$2!)); - break; - default: - throw Exception('unsupported image format: ${image.$1}'); - } - } - await SystemClipboard.instance?.write([item]); - } - - Future setPlainText(String text) async { - await SystemClipboard.instance?.write([ - DataWriterItem()..add(Formats.plainText(text)), - ]); - } - - Future getData() async { - if (_mockData != null) { - return _mockData!; - } - - final reader = await SystemClipboard.instance?.read(); - - if (reader == null) { - return const ClipboardServiceData(); - } - - for (final item in reader.items) { - final availableFormats = await item.rawReader!.getAvailableFormats(); - Log.info('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)); - } else if (reader.canProvide(Formats.jpeg)) { - 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(), - html: html, - image: image, - inAppJson: inAppJson, - tableJson: tableJson, - ); - } -} - -extension on DataReader { - Future? readFile(FileFormat format) { - final c = Completer(); - final progress = getFile( - format, - (file) async { - try { - final all = await file.readAll(); - c.complete(all); - } catch (e) { - c.completeError(e); - } - }, - onError: (e) { - c.completeError(e); - }, - ); - if (progress == null) { - c.complete(null); - } - 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 deleted file mode 100644 index e56ccfc941..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart +++ /dev/null @@ -1,148 +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/plugins.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -/// Copy. -/// -/// - support -/// - desktop -/// - web -/// - mobile -/// -final CommandShortcutEvent customCopyCommand = CommandShortcutEvent( - key: 'copy the selected content', - getDescription: () => AppFlowyEditorL10n.current.cmdCopySelection, - command: 'ctrl+c', - macOSCommand: 'cmd+c', - handler: _copyCommandHandler, -); - -CommandShortcutEventHandler _copyCommandHandler = - (editorState) => handleCopyCommand(editorState); - -KeyEventResult handleCopyCommand( - EditorState editorState, { - bool isCut = false, -}) { - final selection = editorState.selection?.normalized; - if (selection == null) { - return KeyEventResult.ignored; - } - - String? text; - String? html; - String? inAppJson; - - 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; - } - - // plain text. - text = node.delta?.toPlainText(); - - // 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); - } - - () async { - await getIt().setData( - ClipboardServiceData( - plainText: text, - html: html, - inAppJson: inAppJson, - ), - ); - }(); - - 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 deleted file mode 100644 index 9fea34edbf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart +++ /dev/null @@ -1,61 +0,0 @@ -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. -/// -/// - support -/// - desktop -/// - web -/// - mobile -/// -final CommandShortcutEvent customCutCommand = CommandShortcutEvent( - key: 'cut the selected content', - getDescription: () => AppFlowyEditorL10n.current.cmdCutSelection, - command: 'ctrl+x', - macOSCommand: 'cmd+x', - handler: _cutCommandHandler, -); - -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); - } - - 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 deleted file mode 100644 index 6399d3b11f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ /dev/null @@ -1,269 +0,0 @@ -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/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'; - -/// - support -/// - desktop -/// - web -/// - mobile -/// -final CommandShortcutEvent customPasteCommand = CommandShortcutEvent( - key: 'paste the content', - getDescription: () => AppFlowyEditorL10n.current.cmdPasteContent, - command: 'ctrl+v', - macOSCommand: 'cmd+v', - 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(); - } - }); - - 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) { - return; - } - - await editorState.deleteSelectionIfNeeded(); - final result = await editorState.pasteImage( - image.$1, - image.$2!, - documentId, - selection: selection, - ); - if (result) { - return Log.info('Pasted image'); - } - } - - if (html != null && html.isNotEmpty) { - await editorState.deleteSelectionIfNeeded(); - if (await editorState.pasteHtml(html)) { - return Log.info('Pasted html'); - } - } - - if (plainText != null && plainText.isNotEmpty) { - final currentSelection = editorState.selection; - if (currentSelection == null) { - await editorState.updateSelectionWithReason( - selection, - reason: SelectionUpdateReason.uiEvent, - ); - } - await editorState.pasteText(plainText); - return Log.info('Pasted plain text'); - } - - return Log.info('unable to parse the clipboard content'); -} - -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})) { - 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) { - return false; - } - - 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, - ); - - // 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); - - 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/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 deleted file mode 100644 index 3f11759545..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart +++ /dev/null @@ -1,117 +0,0 @@ -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_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 - 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); - } - } - - // 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(); - } - - // 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(), - ); - } -} 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 deleted file mode 100644 index d086f36bed..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ /dev/null @@ -1,184 +0,0 @@ -import 'dart:io'; -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_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:cross_file/cross_file.dart'; -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()), - ); - - 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) { - return false; - } - - if (!defaultImageExtensions.contains(format)) { - Log.info('unsupported format: $format'); - if (UniversalPlatform.isMobile) { - showToastNotification( - message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(), - ); - } - return false; - } - - final isLocalMode = context.read().isLocalMode; - - 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 copyToPath = p.join( - imagePath, - 'tmp_${uuid()}.$format', - ); - await File(copyToPath).writeAsBytes(imageBytes); - final String? path; - - CustomImageType type; - if (isLocalMode) { - path = await saveImageToLocalStorage(copyToPath); - type = CustomImageType.local; - } else { - final result = await saveImageToCloudStorage(copyToPath, documentId); - - final errorMessage = result.$2; - - if (errorMessage != null && context.mounted) { - showToastNotification( - message: errorMessage, - ); - return false; - } - - path = result.$1; - type = CustomImageType.internal; - } - - if (path != null) { - await insertImageNode(path, selection: selection, type: type); - } - - return true; - } catch (e) { - Log.error('cannot copy image file', e); - if (context.mounted) { - showToastNotification( - message: 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 deleted file mode 100644 index d4db86d80c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:convert'; - -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 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; - } catch (e) { - Log.error( - 'Failed to paste in app json: $inAppJson, error: $e', - ); - } - return false; - } -} 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 deleted file mode 100644 index fcb12cefa5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart +++ /dev/null @@ -1,96 +0,0 @@ -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/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 { - await deleteSelectionIfNeeded(); - final nodes = plainText - .split('\n') - .map( - (e) => e - ..replaceAll(r'\r', '') - ..trimRight(), - ) - .map((e) => Delta()..insert(e)) - .map((e) => paragraphNode(delta: e)) - .toList(); - if (nodes.isEmpty) { - return; - } - if (nodes.length == 1) { - await pasteSingleLineNode(nodes.first); - } else { - await pasteMultiLineNodes(nodes.toList()); - } - } - - 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 || - !selection.isSingle || - selection.isCollapsed || - !hrefRegex.hasMatch(plainText)) { - return false; - } - - final node = getNodeAtPath(selection.start.path); - if (node == null) { - return false; - } - - final transaction = this.transaction; - transaction.formatText(node, selection.startIndex, selection.length, { - 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/change_cover_popover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart new file mode 100644 index 0000000000..639af1f979 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart @@ -0,0 +1,535 @@ +import 'dart:io'; +import 'dart:ui'; + +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/image.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/icon_button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +const String kLocalImagesKey = 'local_images'; + +List get builtInAssetImages => [ + "assets/images/app_flowy_abstract_cover_1.jpg", + "assets/images/app_flowy_abstract_cover_2.jpg" + ]; + +class ChangeCoverPopover extends StatefulWidget { + final EditorState editorState; + final Node node; + final Function( + CoverSelectionType selectionType, + String selection, + ) onCoverChanged; + + const ChangeCoverPopover({ + super.key, + required this.editorState, + required this.onCoverChanged, + required this.node, + }); + + @override + State createState() => _ChangeCoverPopoverState(); +} + +class ColorOption { + final String colorHex; + + final String name; + const ColorOption({ + required this.colorHex, + required this.name, + }); +} + +class CoverColorPicker extends StatefulWidget { + final String? selectedBackgroundColorHex; + + final Color pickerBackgroundColor; + final Color pickerItemHoverColor; + final void Function(String color) onSubmittedBackgroundColorHex; + final List backgroundColorOptions; + const CoverColorPicker({ + super.key, + this.selectedBackgroundColorHex, + required this.pickerBackgroundColor, + required this.backgroundColorOptions, + required this.pickerItemHoverColor, + required this.onSubmittedBackgroundColorHex, + }); + + @override + State createState() => _CoverColorPickerState(); +} + +class _ChangeCoverPopoverState extends State { + bool isAddingImage = false; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ChangeCoverPopoverBloc( + editorState: widget.editorState, + node: widget.node, + )..add(const ChangeCoverPopoverEvent.fetchPickedImagePaths()), + child: BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.all(15), + child: SingleChildScrollView( + child: isAddingImage + ? CoverImagePicker( + onBackPressed: () => setState(() { + isAddingImage = false; + }), + onFileSubmit: (List path) { + context.read().add( + const ChangeCoverPopoverEvent + .fetchPickedImagePaths(), + ); + setState(() { + isAddingImage = false; + }); + }, + ) + : _buildCoverSelection(), + ), + ); + }, + ), + ); + } + + Widget _buildCoverSelection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold( + LocaleKeys.document_plugins_cover_colors.tr(), + color: Theme.of(context).colorScheme.tertiary, + ), + const SizedBox(height: 10), + _buildColorPickerList(), + const SizedBox(height: 10), + _buildImageHeader(), + const SizedBox(height: 10), + _buildFileImagePicker(), + const SizedBox(height: 10), + FlowyText.semibold( + LocaleKeys.document_plugins_cover_abstract.tr(), + color: Theme.of(context).colorScheme.tertiary, + ), + const SizedBox(height: 10), + _buildAbstractImagePicker(), + ], + ); + } + + Widget _buildImageHeader() { + return BlocBuilder( + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FlowyText.semibold( + LocaleKeys.document_plugins_cover_images.tr(), + color: Theme.of(context).colorScheme.tertiary, + ), + FlowyTextButton( + fillColor: Theme.of(context).cardColor, + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + LocaleKeys.document_plugins_cover_clearAll.tr(), + fontColor: Theme.of(context).colorScheme.tertiary, + onPressed: () async { + final hasFileImageCover = CoverSelectionType.fromString( + widget.node.attributes[CoverBlockKeys.selectionType], + ) == + CoverSelectionType.file; + final changeCoverBloc = context.read(); + if (hasFileImageCover) { + await showDialog( + context: context, + builder: (context) { + return DeleteImageAlertDialog( + onSubmit: () { + changeCoverBloc.add( + const ChangeCoverPopoverEvent.clearAllImages(), + ); + Navigator.pop(context); + }, + ); + }, + ); + } else { + context + .read() + .add(const ChangeCoverPopoverEvent.clearAllImages()); + } + }, + mainAxisAlignment: MainAxisAlignment.end, + ), + ], + ); + }, + ); + } + + Widget _buildAbstractImagePicker() { + return GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 1 / 0.65, + crossAxisSpacing: 7, + mainAxisSpacing: 7, + ), + itemCount: builtInAssetImages.length, + itemBuilder: (BuildContext ctx, index) { + return InkWell( + onTap: () { + widget.onCoverChanged( + CoverSelectionType.asset, + builtInAssetImages[index], + ); + }, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(builtInAssetImages[index]), + fit: BoxFit.cover, + ), + borderRadius: Corners.s8Border, + ), + ), + ); + }, + ); + } + + Widget _buildColorPickerList() { + final theme = Theme.of(context); + return CoverColorPicker( + pickerBackgroundColor: theme.cardColor, + pickerItemHoverColor: theme.hoverColor, + selectedBackgroundColorHex: + widget.node.attributes[CoverBlockKeys.selectionType] == + CoverSelectionType.color.toString() + ? widget.node.attributes[CoverBlockKeys.selection] + : 'ffffff', + backgroundColorOptions: + _generateBackgroundColorOptions(widget.editorState), + onSubmittedBackgroundColorHex: (color) { + widget.onCoverChanged(CoverSelectionType.color, color); + setState(() {}); + }, + ); + } + + Widget _buildFileImagePicker() { + return BlocBuilder( + builder: (context, state) { + if (state is Loaded) { + final List images = state.imageNames; + return GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 1 / 0.65, + crossAxisSpacing: 7, + mainAxisSpacing: 7, + ), + itemCount: images.length + 1, + itemBuilder: (BuildContext ctx, index) { + if (index == 0) { + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: Corners.s8Border, + ), + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: Icon( + Icons.add, + color: Theme.of(context).colorScheme.primary, + ), + hoverColor: + Theme.of(context).colorScheme.primary.withOpacity(0.15), + width: 20, + onPressed: () { + setState(() { + isAddingImage = true; + }); + }, + ), + ); + } + return ImageGridItem( + onImageSelect: () { + widget.onCoverChanged( + CoverSelectionType.file, + images[index - 1], + ); + }, + onImageDelete: () async { + final changeCoverBloc = + context.read(); + final deletingCurrentCover = + widget.node.attributes[CoverBlockKeys.selection] == + images[index - 1]; + if (deletingCurrentCover) { + await showDialog( + context: context, + builder: (context) { + return DeleteImageAlertDialog( + onSubmit: () { + changeCoverBloc.add( + ChangeCoverPopoverEvent.deleteImage( + images[index - 1], + ), + ); + Navigator.pop(context); + }, + ); + }, + ); + } else { + changeCoverBloc.add(DeleteImage(images[index - 1])); + } + }, + imagePath: images[index - 1], + ); + }, + ); + } + return Container(); + }, + ); + } + + List _generateBackgroundColorOptions(EditorState editorState) { + return FlowyTint.values + .map( + (t) => ColorOption( + colorHex: t.color(context).toHex(), + name: t.tintName(AppFlowyEditorLocalizations.current), + ), + ) + .toList(); + } +} + +class DeleteImageAlertDialog extends StatelessWidget { + const DeleteImageAlertDialog({ + Key? key, + required this.onSubmit, + }) : super(key: key); + + final Function() onSubmit; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: FlowyText.semibold( + "Image is used in cover", + fontSize: 20, + color: Theme.of(context).colorScheme.tertiary, + ), + content: Container( + constraints: const BoxConstraints(minHeight: 100), + padding: const EdgeInsets.symmetric( + vertical: 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text(LocaleKeys.document_plugins_cover_coverRemoveAlert).tr(), + const SizedBox( + height: 4, + ), + const Text( + LocaleKeys.document_plugins_cover_alertDialogConfirmation, + ).tr(), + ], + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 20.0, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text(LocaleKeys.button_Cancel).tr(), + ), + TextButton( + onPressed: onSubmit, + child: const Text(LocaleKeys.button_OK).tr(), + ), + ], + ); + } +} + +class ImageGridItem extends StatefulWidget { + const ImageGridItem({ + Key? key, + required this.onImageSelect, + required this.onImageDelete, + required this.imagePath, + }) : super(key: key); + + final Function() onImageSelect; + final Function() onImageDelete; + final String imagePath; + + @override + State createState() => _ImageGridItemState(); +} + +class _ImageGridItemState extends State { + bool showDeleteButton = false; + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) { + setState(() { + showDeleteButton = true; + }); + }, + onExit: (_) { + setState(() { + showDeleteButton = false; + }); + }, + child: Stack( + children: [ + InkWell( + onTap: widget.onImageSelect, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: FileImage(File(widget.imagePath)), + fit: BoxFit.cover, + ), + borderRadius: Corners.s8Border, + ), + ), + ), + if (showDeleteButton) + Positioned( + right: 2, + top: 2, + child: FlowyIconButton( + fillColor: + Theme.of(context).colorScheme.surface.withOpacity(0.8), + hoverColor: + Theme.of(context).colorScheme.surface.withOpacity(0.8), + iconPadding: const EdgeInsets.all(5), + width: 28, + icon: svgWidget( + 'editor/delete', + color: Theme.of(context).colorScheme.tertiary, + ), + onPressed: widget.onImageDelete, + ), + ), + ], + ), + ); + } +} + +class _CoverColorPickerState extends State { + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Container( + height: 30, + alignment: Alignment.center, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + platform: TargetPlatform.windows, + ), + child: SingleChildScrollView( + child: _buildColorItems( + widget.backgroundColorOptions, + widget.selectedBackgroundColorHex, + ), + ), + ), + ); + } + + @override + void dispose() { + super.dispose(); + scrollController.dispose(); + } + + Widget _buildColorItem(ColorOption option, bool isChecked) { + return InkWell( + customBorder: const RoundedRectangleBorder( + borderRadius: Corners.s6Border, + ), + hoverColor: widget.pickerItemHoverColor, + onTap: () { + widget.onSubmittedBackgroundColorHex(option.colorHex); + }, + child: Padding( + padding: const EdgeInsets.only(right: 10.0), + child: SizedBox.square( + dimension: isChecked ? 24 : 25, + child: Container( + decoration: BoxDecoration( + color: option.colorHex.toColor(), + border: isChecked + ? Border.all( + color: const Color(0xFFFFFFFF), + width: 2.0, + ) + : null, + shape: BoxShape.circle, + ), + child: isChecked + ? SizedBox.square( + dimension: 24, + child: Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: option.colorHex.toColor(), + shape: BoxShape.circle, + ), + ), + ) + : null, + ), + ), + ), + ); + } + + Widget _buildColorItems(List options, String? selectedColor) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: options + .map((e) => _buildColorItem(e, e.colorHex == selectedColor)) + .toList(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover_bloc.dart new file mode 100644 index 0000000000..18e2abc019 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/change_cover_popover_bloc.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'change_cover_popover_bloc.freezed.dart'; + +class ChangeCoverPopoverBloc + extends Bloc { + final EditorState editorState; + final Node node; + late final SharedPreferences _prefs; + final _initCompleter = Completer(); + ChangeCoverPopoverBloc({required this.editorState, required this.node}) + : super(const ChangeCoverPopoverState.initial()) { + SharedPreferences.getInstance().then((prefs) { + _prefs = prefs; + _initCompleter.complete(); + }); + on((event, emit) async { + await event.map( + fetchPickedImagePaths: + (FetchPickedImagePaths fetchPickedImagePaths) async { + final imageNames = await _getPreviouslyPickedImagePaths(); + emit(ChangeCoverPopoverState.loaded(imageNames)); + }, + deleteImage: (DeleteImage deleteImage) async { + final currentState = state; + final currentlySelectedImage = + node.attributes[CoverBlockKeys.selection]; + if (currentState is Loaded) { + await _deleteImageInStorage(deleteImage.path); + if (currentlySelectedImage == deleteImage.path) { + _removeCoverImageFromNode(); + } + final updateImageList = currentState.imageNames + .where((path) => path != deleteImage.path) + .toList(); + await _updateImagePathsInStorage(updateImageList); + emit(Loaded(updateImageList)); + } + }, + clearAllImages: (ClearAllImages clearAllImages) async { + final currentState = state; + final currentlySelectedImage = + node.attributes[CoverBlockKeys.selection]; + + if (currentState is Loaded) { + for (final image in currentState.imageNames) { + await _deleteImageInStorage(image); + if (currentlySelectedImage == image) { + _removeCoverImageFromNode(); + } + } + await _updateImagePathsInStorage([]); + emit(const Loaded([])); + } + }, + ); + }); + } + + Future> _getPreviouslyPickedImagePaths() async { + await _initCompleter.future; + final imageNames = _prefs.getStringList(kLocalImagesKey) ?? []; + if (imageNames.isEmpty) { + return imageNames; + } + imageNames.removeWhere((name) => !File(name).existsSync()); + _prefs.setStringList(kLocalImagesKey, imageNames); + return imageNames; + } + + Future _updateImagePathsInStorage(List imagePaths) async { + await _initCompleter.future; + _prefs.setStringList(kLocalImagesKey, imagePaths); + return; + } + + Future _deleteImageInStorage(String path) async { + final imageFile = File(path); + await imageFile.delete(); + } + + Future _removeCoverImageFromNode() async { + final transaction = editorState.transaction; + transaction.updateNode(node, { + CoverBlockKeys.selectionType: CoverSelectionType.initial.toString(), + CoverBlockKeys.iconSelection: + node.attributes[CoverBlockKeys.iconSelection] + }); + return editorState.apply(transaction); + } +} + +@freezed +class ChangeCoverPopoverEvent with _$ChangeCoverPopoverEvent { + const factory ChangeCoverPopoverEvent.fetchPickedImagePaths() = + FetchPickedImagePaths; + + 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.loaded( + List imageNames, + ) = Loaded; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker.dart new file mode 100644 index 0000000000..e7acb61403 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker.dart @@ -0,0 +1,336 @@ +import 'dart:io'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:flowy_infra/image.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/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; + +class CoverImagePicker extends StatefulWidget { + final VoidCallback onBackPressed; + final Function(List paths) onFileSubmit; + + const CoverImagePicker({ + super.key, + required this.onBackPressed, + required this.onFileSubmit, + }); + + @override + State createState() => _CoverImagePickerState(); +} + +class _CoverImagePickerState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CoverImagePickerBloc() + ..add(const CoverImagePickerEvent.initialEvent()), + child: BlocListener( + listener: (context, state) { + if (state is NetworkImagePicked) { + state.successOrFail.isRight() + ? showSnapBar( + context, + LocaleKeys.document_plugins_cover_invalidImageUrl.tr(), + ) + : null; + } + 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 is Loading + ? const SizedBox( + height: 180, + child: Center( + child: CircularProgressIndicator(), + ), + ) + : CoverImagePreviewWidget(state: state), + const SizedBox( + height: 10, + ), + NetworkImageUrlInput( + onAdd: (url) { + context.read().add(UrlSubmit(url)); + }, + ), + const SizedBox( + height: 10, + ), + ImagePickerActionButtons( + onBackPressed: () { + widget.onBackPressed(); + }, + onSave: () { + context.read().add( + SaveToGallery(state), + ); + }, + ), + ], + ); + }, + ), + ), + ); + } +} + +class NetworkImageUrlInput extends StatefulWidget { + final void Function(String color) onAdd; + + const NetworkImageUrlInput({ + super.key, + required this.onAdd, + }); + + @override + State createState() => _NetworkImageUrlInputState(); +} + +class _NetworkImageUrlInputState extends State { + TextEditingController urlController = TextEditingController(); + bool get buttonDisabled => urlController.text.isEmpty; + + @override + void initState() { + super.initState(); + urlController.addListener(() { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + flex: 4, + child: FlowyTextField( + controller: urlController, + hintText: LocaleKeys.document_plugins_cover_enterImageUrl.tr(), + ), + ), + const SizedBox( + width: 5, + ), + Expanded( + flex: 1, + child: RoundedTextButton( + onPressed: () { + urlController.text.isNotEmpty + ? widget.onAdd(urlController.text) + : null; + }, + hoverColor: Colors.transparent, + fillColor: buttonDisabled + ? Theme.of(context).disabledColor + : Theme.of(context).colorScheme.primary, + height: 36, + title: LocaleKeys.document_plugins_cover_add.tr(), + borderRadius: Corners.s8Border, + ), + ) + ], + ); + } +} + +class ImagePickerActionButtons extends StatelessWidget { + final VoidCallback onBackPressed; + final VoidCallback onSave; + + const ImagePickerActionButtons({ + super.key, + required this.onBackPressed, + required this.onSave, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FlowyTextButton( + LocaleKeys.document_plugins_cover_back.tr(), + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.end, + onPressed: () => onBackPressed(), + ), + FlowyTextButton( + LocaleKeys.document_plugins_cover_saveToGallery.tr(), + onPressed: () => onSave(), + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.end, + fontColor: Theme.of(context).colorScheme.primary, + ), + ], + ); + } +} + +class CoverImagePreviewWidget extends StatefulWidget { + final dynamic state; + + const CoverImagePreviewWidget({super.key, required this.state}); + + @override + State createState() => + _CoverImagePreviewWidgetState(); +} + +class _CoverImagePreviewWidgetState extends State { + _buildFilePickerWidget(BuildContext ctx) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: Corners.s6Border, + border: Border.fromBorderSide( + BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 1, + ), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowySvg( + name: 'editor/add', + size: Size(20, 20), + ), + const SizedBox( + width: 3, + ), + FlowyText( + LocaleKeys.document_plugins_cover_pasteImageUrl.tr(), + ), + ], + ), + const SizedBox( + height: 10, + ), + FlowyText( + LocaleKeys.document_plugins_cover_or.tr(), + fontWeight: FontWeight.w300, + ), + const SizedBox( + height: 10, + ), + FlowyButton( + hoverColor: Theme.of(context).hoverColor, + onTap: () { + ctx.read().add(const PickFileImage()); + }, + useIntrinsicWidth: true, + leftIcon: const FlowySvg( + name: 'file_icon', + size: Size(20, 20), + ), + text: FlowyText( + LocaleKeys.document_plugins_cover_pickFromFiles.tr(), + ), + ), + ], + ), + ); + } + + _buildImageDeleteButton(BuildContext ctx) { + return Positioned( + right: 10, + top: 10, + child: InkWell( + onTap: () { + ctx.read().add(const DeleteImage()); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.onPrimary, + ), + child: svgWidget( + "editor/close", + size: const Size(20, 20), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + height: 180, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: Corners.s6Border, + 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 is FileImagePicked) + ? _buildImageDeleteButton(context) + : (widget.state is NetworkImagePicked) + ? widget.state.successOrFail.fold( + (l) => _buildImageDeleteButton(context), + (r) => Container(), + ) + : Container() + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart new file mode 100644 index 0000000000..68ffe4e966 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart @@ -0,0 +1,215 @@ +import 'dart:io'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:file_picker/file_picker.dart' as fp; + +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:path/path.dart' as p; +import 'change_cover_popover.dart'; + +part 'cover_image_picker_bloc.freezed.dart'; + +class CoverImagePickerBloc + extends Bloc { + static const allowedExtensions = ['jpg', 'png', 'jpeg']; + + CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) { + on( + (event, emit) async { + await event.map( + initialEvent: (InitialEvent initialEvent) { + emit(const CoverImagePickerState.initial()); + }, + urlSubmit: (UrlSubmit urlSubmit) async { + emit(const CoverImagePickerState.loading()); + final validateImage = await _validateURL(urlSubmit.path); + if (validateImage) { + emit(CoverImagePickerState.networkImage(left(urlSubmit.path))); + } else { + emit( + CoverImagePickerState.networkImage( + right( + FlowyError( + msg: LocaleKeys.document_plugins_cover_couldNotFetchImage + .tr(), + ), + ), + ), + ); + } + }, + pickFileImage: (PickFileImage pickFileImage) async { + final imagePickerResults = await _pickImages(); + if (imagePickerResults != null) { + emit(CoverImagePickerState.fileImage(imagePickerResults)); + } else { + emit(const CoverImagePickerState.initial()); + } + }, + deleteImage: (DeleteImage deleteImage) { + emit(const CoverImagePickerState.initial()); + }, + saveToGallery: (SaveToGallery saveToGallery) async { + emit(const CoverImagePickerState.loading()); + final saveImage = await _saveToGallery(saveToGallery.previousState); + if (saveImage != null) { + emit(CoverImagePickerState.done(left(saveImage))); + } else { + emit( + CoverImagePickerState.done( + right( + FlowyError( + msg: LocaleKeys.document_plugins_cover_imageSavingFailed + .tr(), + ), + ), + ), + ); + emit(const CoverImagePickerState.initial()); + } + }, + ); + }, + ); + } + + _saveToGallery(CoverImagePickerState state) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List imagePaths = prefs.getStringList(kLocalImagesKey) ?? []; + final directory = await _coverPath(); + + if (state is FileImagePicked) { + try { + final path = state.path; + final newPath = p.join(directory, p.split(path).last); + final newFile = await File(path).copy(newPath); + imagePaths.add(newFile.path); + } catch (e) { + return null; + } + } else if (state is NetworkImagePicked) { + try { + final url = state.successOrFail.fold((path) => path, (r) => null); + if (url != null) { + final response = await http.get(Uri.parse(url)); + final newPath = p.join(directory, _networkImageName(url)); + final imageFile = File(newPath); + await imageFile.create(); + await imageFile.writeAsBytes(response.bodyBytes); + imagePaths.add(imageFile.absolute.path); + } else { + return null; + } + } catch (e) { + return null; + } + } + await prefs.setStringList(kLocalImagesKey, imagePaths); + return imagePaths; + } + + Future _pickImages() async { + final result = await getIt().pickFiles( + dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(), + allowMultiple: false, + type: fp.FileType.image, + allowedExtensions: allowedExtensions, + ); + if (result != null && result.files.isNotEmpty) { + return result.files.first.path; + } + return null; + } + + Future _coverPath() async { + final directory = await getIt().getPath(); + return Directory(p.join(directory, 'covers')) + .create(recursive: true) + .then((value) => value.path); + } + + String _networkImageName(String url) { + return 'IMG_${DateTime.now().millisecondsSinceEpoch.toString()}.${_getExtension( + url, + fromNetwork: true, + )}'; + } + + String? _getExtension( + String path, { + bool fromNetwork = false, + }) { + String? ext; + if (!fromNetwork) { + final extension = p.extension(path); + if (extension.isEmpty) { + return null; + } + ext = extension; + } else { + final uri = Uri.parse(path); + final parameters = uri.queryParameters; + if (path.contains('unsplash')) { + final dl = parameters['dl']; + if (dl != null) { + ext = p.extension(dl); + } + } else { + ext = p.extension(path); + } + } + if (ext != null && ext.isNotEmpty) { + ext = ext.substring(1); + } + if (allowedExtensions.contains(ext)) { + return ext; + } + return null; + } + + Future _validateURL(String path) async { + final extension = _getExtension(path, fromNetwork: true); + if (extension == null) { + return false; + } + try { + final response = await http.head(Uri.parse(path)); + return response.statusCode == 200; + } catch (e) { + return false; + } + } +} + +@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.saveToGallery( + CoverImagePickerState previousState, + ) = SaveToGallery; + const factory CoverImagePickerEvent.initialEvent() = InitialEvent; +} + +@freezed +class CoverImagePickerState with _$CoverImagePickerState { + const factory CoverImagePickerState.initial() = Initial; + const factory CoverImagePickerState.loading() = Loading; + const factory CoverImagePickerState.networkImage( + Either successOrFail, + ) = NetworkImagePicked; + const factory CoverImagePickerState.fileImage(String path) = FileImagePicked; + + const factory CoverImagePickerState.done( + Either, FlowyError> successOrFail, + ) = Done; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart new file mode 100644 index 0000000000..64d92e1125 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart @@ -0,0 +1,555 @@ +import 'dart:io'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart'; +import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; + +class CoverBlockKeys { + const CoverBlockKeys._(); + + static const String selectionType = 'cover_selection_type'; + static const String selection = 'cover_selection'; + static const String iconSelection = 'selected_icon'; +} + +enum CoverSelectionType { + initial, + color, + file, + asset; + + static CoverSelectionType fromString(String? value) { + if (value == null) { + return CoverSelectionType.initial; + } + return CoverSelectionType.values.firstWhere( + (e) => e.toString() == value, + orElse: () => CoverSelectionType.initial, + ); + } +} + +class CoverNodeWidgetBuilder implements NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return CoverImageNodeWidget( + key: context.node.key, + node: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => (node) { + return true; + }; +} + +class CoverImageNodeWidget extends StatefulWidget { + const CoverImageNodeWidget({ + Key? key, + required this.node, + required this.editorState, + }) : super(key: key); + + final Node node; + final EditorState editorState; + + @override + State createState() => _CoverImageNodeWidgetState(); +} + +class _CoverImageNodeWidgetState extends State { + CoverSelectionType get selectionType => CoverSelectionType.fromString( + widget.node.attributes[CoverBlockKeys.selectionType], + ); + + @override + void initState() { + super.initState(); + + widget.node.addListener(_reload); + } + + @override + void dispose() { + widget.node.removeListener(_reload); + + super.dispose(); + } + + void _reload() { + setState(() {}); + } + + PopoverController iconPopoverController = PopoverController(); + @override + Widget build(BuildContext context) { + return _CoverImage( + editorState: widget.editorState, + node: widget.node, + onCoverChanged: (type, value) { + _insertCover(type, value); + }, + ); + } + + Future _insertCover(CoverSelectionType type, dynamic cover) async { + final transaction = widget.editorState.transaction; + transaction.updateNode(widget.node, { + CoverBlockKeys.selectionType: type.toString(), + CoverBlockKeys.selection: cover, + CoverBlockKeys.iconSelection: + widget.node.attributes[CoverBlockKeys.iconSelection] + }); + return widget.editorState.apply(transaction); + } +} + +class _AddCoverButton extends StatefulWidget { + final Node node; + final EditorState editorState; + final bool hasIcon; + final CoverSelectionType selectionType; + + final PopoverController iconPopoverController; + const _AddCoverButton({ + required this.onTap, + required this.node, + required this.editorState, + required this.hasIcon, + required this.selectionType, + required this.iconPopoverController, + }); + + final VoidCallback onTap; + + @override + State<_AddCoverButton> createState() => _AddCoverButtonState(); +} + +bool isPopoverOpen = false; + +class _AddCoverButtonState extends State<_AddCoverButton> { + bool isHidden = true; + PopoverMutex mutex = PopoverMutex(); + bool isPopoverOpen = false; + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (event) { + setHidden(false); + }, + onExit: (event) { + setHidden(isPopoverOpen ? false : true); + }, + opaque: false, + child: Container( + height: widget.hasIcon ? 180 : 50.0, + alignment: Alignment.bottomLeft, + width: double.infinity, + padding: const EdgeInsets.only( + left: 80, + top: 20, + bottom: 5, + ), + child: isHidden + ? Container() + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Add Cover Button. + widget.selectionType != CoverSelectionType.initial + ? Container() + : FlowyButton( + key: UniqueKey(), + leftIconSize: const Size.square(18), + onTap: widget.onTap, + useIntrinsicWidth: true, + leftIcon: const FlowySvg(name: 'editor/image'), + text: FlowyText.regular( + LocaleKeys.document_plugins_cover_addCover.tr(), + ), + ), + // Add Icon Button. + widget.hasIcon + ? FlowyButton( + leftIconSize: const Size.square(18), + onTap: () { + _removeIcon(); + }, + useIntrinsicWidth: true, + leftIcon: const Icon( + Icons.emoji_emotions_outlined, + size: 18, + ), + text: FlowyText.regular( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + ), + ) + : AppFlowyPopover( + mutex: mutex, + asBarrier: true, + onClose: () { + isPopoverOpen = false; + setHidden(true); + }, + offset: const Offset(120, 10), + controller: widget.iconPopoverController, + direction: PopoverDirection.bottomWithCenterAligned, + constraints: + BoxConstraints.loose(const Size(320, 380)), + margin: EdgeInsets.zero, + child: FlowyButton( + leftIconSize: const Size.square(18), + useIntrinsicWidth: true, + leftIcon: const Icon( + Icons.emoji_emotions_outlined, + size: 18, + ), + text: FlowyText.regular( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ), + ), + popupBuilder: (BuildContext popoverContext) { + isPopoverOpen = true; + return EmojiPopover( + showRemoveButton: widget.hasIcon, + removeIcon: _removeIcon, + node: widget.node, + editorState: widget.editorState, + onEmojiChanged: (Emoji emoji) { + _insertIcon(emoji); + widget.iconPopoverController.close(); + }, + ); + }, + ) + ], + ), + ), + ); + } + + Future _insertIcon(Emoji emoji) async { + final transaction = widget.editorState.transaction; + transaction.updateNode(widget.node, { + CoverBlockKeys.selectionType: + widget.node.attributes[CoverBlockKeys.selectionType], + CoverBlockKeys.selection: + widget.node.attributes[CoverBlockKeys.selection], + CoverBlockKeys.iconSelection: emoji.emoji, + }); + return widget.editorState.apply(transaction); + } + + Future _removeIcon() async { + final transaction = widget.editorState.transaction; + transaction.updateNode(widget.node, { + CoverBlockKeys.iconSelection: "", + CoverBlockKeys.selectionType: + widget.node.attributes[CoverBlockKeys.selectionType], + CoverBlockKeys.selection: + widget.node.attributes[CoverBlockKeys.selection], + }); + return widget.editorState.apply(transaction); + } + + void setHidden(bool value) { + if (isHidden == value) return; + setState(() { + isHidden = value; + }); + } +} + +class _CoverImage extends StatefulWidget { + const _CoverImage({ + required this.editorState, + required this.node, + required this.onCoverChanged, + }); + + final Node node; + final EditorState editorState; + final Function( + CoverSelectionType selectionType, + dynamic selection, + ) onCoverChanged; + @override + State<_CoverImage> createState() => _CoverImageState(); +} + +class _CoverImageState extends State<_CoverImage> { + final popoverController = PopoverController(); + + CoverSelectionType get selectionType => CoverSelectionType.fromString( + widget.node.attributes[CoverBlockKeys.selectionType], + ); + Color get color { + final hex = widget.node.attributes[CoverBlockKeys.selection] as String?; + return hex?.toColor() ?? Colors.white; + } + + bool get hasIcon => + widget.node.attributes[CoverBlockKeys.iconSelection] == null + ? false + : widget.node.attributes[CoverBlockKeys.iconSelection].isNotEmpty; + bool isOverlayButtonsHidden = true; + PopoverController iconPopoverController = PopoverController(); + bool get hasCover => + selectionType == CoverSelectionType.initial ? false : true; + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.bottomLeft, + children: [ + Container( + alignment: Alignment.topCenter, + height: !hasCover + ? 0 + : hasIcon + ? 320 + : 280, + child: _buildCoverImage(context, widget.editorState), + ), + hasIcon + ? Positioned( + left: 80, + bottom: !hasCover ? 30 : 40, + child: AppFlowyPopover( + offset: const Offset(100, 0), + controller: iconPopoverController, + direction: PopoverDirection.bottomWithCenterAligned, + constraints: BoxConstraints.loose(const Size(320, 380)), + margin: EdgeInsets.zero, + child: EmojiIconWidget( + emoji: widget.node.attributes[CoverBlockKeys.iconSelection], + ), + popupBuilder: (BuildContext popoverContext) { + return EmojiPopover( + node: widget.node, + showRemoveButton: hasIcon, + removeIcon: _removeIcon, + editorState: widget.editorState, + onEmojiChanged: (Emoji emoji) { + _insertIcon(emoji); + iconPopoverController.close(); + }, + ); + }, + ), + ) + : Container(), + hasIcon && selectionType != CoverSelectionType.initial + ? Container() + : _AddCoverButton( + onTap: () { + _insertCover( + CoverSelectionType.asset, + builtInAssetImages.first, + ); + }, + node: widget.node, + editorState: widget.editorState, + hasIcon: hasIcon, + selectionType: selectionType, + iconPopoverController: iconPopoverController, + ), + ], + ); + } + + Future _insertCover(CoverSelectionType type, dynamic cover) async { + final transaction = widget.editorState.transaction; + transaction.updateNode(widget.node, { + CoverBlockKeys.selectionType: type.toString(), + CoverBlockKeys.selection: cover, + CoverBlockKeys.iconSelection: + widget.node.attributes[CoverBlockKeys.iconSelection] + }); + return widget.editorState.apply(transaction); + } + + Future _insertIcon(Emoji emoji) async { + final transaction = widget.editorState.transaction; + transaction.updateNode(widget.node, { + CoverBlockKeys.selectionType: + widget.node.attributes[CoverBlockKeys.selectionType], + CoverBlockKeys.selection: + widget.node.attributes[CoverBlockKeys.selection], + CoverBlockKeys.iconSelection: emoji.emoji, + }); + return widget.editorState.apply(transaction); + } + + Future _removeIcon() async { + final transaction = widget.editorState.transaction; + transaction.updateNode(widget.node, { + CoverBlockKeys.iconSelection: "", + CoverBlockKeys.selectionType: + widget.node.attributes[CoverBlockKeys.selectionType], + CoverBlockKeys.selection: + widget.node.attributes[CoverBlockKeys.selection], + }); + return widget.editorState.apply(transaction); + } + + Widget _buildCoverOverlayButtons(BuildContext context) { + return Positioned( + bottom: 20, + right: 50, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AppFlowyPopover( + onClose: () { + setOverlayButtonsHidden(true); + }, + offset: const Offset(-125, 10), + controller: popoverController, + direction: PopoverDirection.bottomWithCenterAligned, + constraints: BoxConstraints.loose(const Size(380, 450)), + margin: EdgeInsets.zero, + child: Visibility( + maintainState: true, + maintainAnimation: true, + maintainSize: true, + visible: !isOverlayButtonsHidden, + child: RoundedTextButton( + onPressed: () { + popoverController.show(); + setOverlayButtonsHidden(true); + }, + hoverColor: Theme.of(context).colorScheme.surface, + textColor: Theme.of(context).colorScheme.tertiary, + fillColor: + Theme.of(context).colorScheme.surface.withOpacity(0.5), + width: 120, + height: 28, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), + ), + popupBuilder: (BuildContext popoverContext) { + return ChangeCoverPopover( + node: widget.node, + editorState: widget.editorState, + onCoverChanged: widget.onCoverChanged, + ); + }, + ), + const SizedBox(width: 10), + Visibility( + maintainAnimation: true, + maintainSize: true, + maintainState: true, + visible: !isOverlayButtonsHidden, + child: FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.surface, + fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5), + iconPadding: const EdgeInsets.all(5), + width: 28, + icon: svgWidget( + 'editor/delete', + color: Theme.of(context).colorScheme.tertiary, + ), + onPressed: () { + widget.onCoverChanged(CoverSelectionType.initial, null); + }, + ), + ), + ], + ), + ); + } + + Widget _buildCoverImage(BuildContext context, EditorState editorState) { + const height = 250.0; + final Widget coverImage; + switch (selectionType) { + case CoverSelectionType.file: + final imageFile = + File(widget.node.attributes[CoverBlockKeys.selection]); + if (!imageFile.existsSync()) { + // reset cover state + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onCoverChanged(CoverSelectionType.initial, null); + }); + coverImage = const SizedBox(); + break; + } + coverImage = Image.file( + imageFile, + fit: BoxFit.cover, + ); + break; + case CoverSelectionType.asset: + coverImage = Image.asset( + widget.node.attributes[CoverBlockKeys.selection], + fit: BoxFit.cover, + ); + break; + case CoverSelectionType.color: + coverImage = Container( + decoration: BoxDecoration( + color: color, + borderRadius: Corners.s6Border, + ), + alignment: Alignment.center, + ); + break; + case CoverSelectionType.initial: + coverImage = const SizedBox(); + break; + } +// OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error + return MouseRegion( + onEnter: (event) { + setOverlayButtonsHidden(false); + }, + onExit: (event) { + setOverlayButtonsHidden(true); + }, + child: SizedBox( + height: height, + child: Stack( + children: [ + Container( + padding: const EdgeInsets.only(bottom: 10), + height: double.infinity, + width: double.infinity, + child: coverImage, + ), + hasCover + ? _buildCoverOverlayButtons(context) + : const SizedBox.shrink() + ], + ), + ), + ); + } + + void setOverlayButtonsHidden(bool value) { + if (isOverlayButtonsHidden == value) return; + setState(() { + isOverlayButtonsHidden = value; + }); + } +} 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 deleted file mode 100644 index 96d39d7500..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ /dev/null @@ -1,358 +0,0 @@ -import 'dart:io'; - -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/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/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'; -import 'package:auto_size_text_field/auto_size_text_field.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/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; - -class DocumentImmersiveCover extends StatefulWidget { - const DocumentImmersiveCover({ - 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(); -} - -class _DocumentImmersiveCoverState extends State { - final textEditingController = TextEditingController(); - final scrollController = ScrollController(); - final focusNode = FocusNode(); - - late PropertyValueNotifier? selectionNotifier = - context.read().state.editorState?.selectionNotifier; - - @override - void initState() { - super.initState(); - selectionNotifier?.addListener(_unfocus); - if (widget.view.name.isEmpty) { - focusNode.requestFocus(); - } - } - - @override - void dispose() { - textEditingController.dispose(); - scrollController.dispose(); - selectionNotifier?.removeListener(_unfocus); - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return IgnoreParentGestureWidget( - child: BlocProvider( - create: (context) => DocumentImmersiveCoverBloc(view: widget.view) - ..add(const DocumentImmersiveCoverEvent.initial()), - child: BlocConsumer( - listener: (context, state) { - if (textEditingController.text != state.name) { - textEditingController.text = state.name; - } - }, - builder: (_, state) { - final iconAndTitle = _buildIconAndTitle(context, state); - if (state.cover.type == PageStyleCoverImageType.none) { - return Padding( - padding: EdgeInsets.only( - top: context.statusBarAndAppBarHeight + kDocumentTitlePadding, - ), - child: iconAndTitle, - ); - } - - 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, - ), - ), - ], - ), - ); - }, - ), - ), - ); - } - - Widget _buildIconAndTitle( - BuildContext context, - DocumentImmersiveCoverState state, - ) { - final icon = state.icon; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Row( - children: [ - if (icon != null && icon.isNotEmpty) ...[ - _buildIcon(context, icon), - const HSpace(8.0), - ], - Expanded(child: _buildTitle(context, state)), - ], - ), - ); - } - - Widget _buildTitle( - BuildContext context, - DocumentImmersiveCoverState state, - ) { - String? fontFamily = defaultFontFamily; - final documentFontFamily = - context.read().state.fontFamily; - if (documentFontFamily != null && fontFamily != documentFontFamily) { - 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( - border: InputBorder.none, - enabledBorder: InputBorder.none, - disabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - hintText: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - contentPadding: EdgeInsets.zero, - ), - scrollController: scrollController, - style: TextStyle( - fontSize: 28.0, - fontWeight: FontWeight.w700, - fontFamily: fontFamily, - color: - 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), - ); - }, - ); - } - - Widget _buildIcon(BuildContext context, EmojiIconData icon) { - return GestureDetector( - child: ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 34.0), - child: EmojiIconWidget( - emoji: icon, - emojiSize: 26, - ), - ), - onTap: () async { - final pageStyleIconBloc = PageStyleIconBloc(view: widget.view) - ..add(const PageStyleIconEvent.initial()); - await 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 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); - }, - ), - ), - ); - }, - builder: (_) => const SizedBox.shrink(), - ); - }, - ); - } - - Widget _buildCover(BuildContext context, DocumentImmersiveCoverState state) { - final cover = state.cover; - final type = cover.type; - final naviBarHeight = MediaQuery.of(context).padding.top; - final height = naviBarHeight + kDocumentCoverHeight; - - if (type == PageStyleCoverImageType.customImage || - type == PageStyleCoverImageType.unsplashImage) { - return SizedBox( - height: height, - width: double.infinity, - child: FlowyNetworkImage( - url: cover.value, - userProfilePB: widget.userProfilePB, - ), - ); - } - - if (type == PageStyleCoverImageType.builtInImage) { - return SizedBox( - height: height, - width: double.infinity, - child: Image.asset( - PageStyleCoverImageType.builtInImagePath(cover.value), - fit: BoxFit.cover, - ), - ); - } - - if (type == PageStyleCoverImageType.pureColor) { - return Container( - height: height, - width: double.infinity, - color: cover.value.coverColor(context), - ); - } - - if (type == PageStyleCoverImageType.gradientColor) { - return Container( - height: height, - width: double.infinity, - decoration: BoxDecoration( - gradient: FlowyGradientColor.fromId(cover.value).linear, - ), - ); - } - - if (type == PageStyleCoverImageType.localImage) { - return SizedBox( - height: height, - width: double.infinity, - child: Image.file( - File(cover.value), - fit: BoxFit.cover, - ), - ); - } - - return SizedBox( - height: naviBarHeight, - width: double.infinity, - ); - } - - void _unfocus() { - final selection = selectionNotifier?.value; - if (selection != null) { - focusNode.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild); - } - } - - void _rename(String name) { - 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 deleted file mode 100644 index 3006fc3104..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart +++ /dev/null @@ -1,92 +0,0 @@ -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 - extends Bloc { - DocumentImmersiveCoverBloc({ - required this.view, - }) : _viewListener = ViewListener(viewId: view.id), - super(DocumentImmersiveCoverState.initial()) { - on( - (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.name, - ), - ); - _viewListener?.start( - onViewUpdated: (view) { - add( - DocumentImmersiveCoverEvent.updateCoverAndIcon( - view.cover, - EmojiIconData.fromViewIconPB(view.icon), - view.name, - ), - ); - }, - ); - }, - updateCoverAndIcon: (cover, icon, name) { - emit( - state.copyWith( - icon: icon, - cover: cover ?? state.cover, - name: name ?? state.name, - ), - ); - }, - ); - }, - ); - } - - final ViewPB view; - final ViewListener? _viewListener; - - @override - Future close() { - _viewListener?.stop(); - return super.close(); - } -} - -@freezed -class DocumentImmersiveCoverEvent with _$DocumentImmersiveCoverEvent { - const factory DocumentImmersiveCoverEvent.initial() = Initial; - - const factory DocumentImmersiveCoverEvent.updateCoverAndIcon( - PageStyleCover? cover, - EmojiIconData? icon, - String? name, - ) = UpdateCoverAndIcon; -} - -@freezed -class DocumentImmersiveCoverState with _$DocumentImmersiveCoverState { - const factory DocumentImmersiveCoverState({ - @Default(null) EmojiIconData? icon, - required PageStyleCover cover, - @Default('') String name, - }) = _DocumentImmersiveCoverState; - - factory DocumentImmersiveCoverState.initial() => DocumentImmersiveCoverState( - cover: PageStyleCover.none(), - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart new file mode 100644 index 0000000000..2d3f3b5991 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart @@ -0,0 +1,53 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class EmojiIconWidget extends StatefulWidget { + const EmojiIconWidget({ + super.key, + required this.emoji, + this.size = 80, + this.emojiSize = 60, + }); + + final String emoji; + final double size; + final double emojiSize; + + @override + State createState() => _EmojiIconWidgetState(); +} + +class _EmojiIconWidgetState extends State { + bool hover = true; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setHidden(false), + onExit: (_) => setHidden(true), + child: Container( + height: widget.size, + width: widget.size, + decoration: BoxDecoration( + color: !hover + ? Theme.of(context).colorScheme.inverseSurface + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: FlowyText( + widget.emoji, + fontSize: widget.emojiSize, + textAlign: TextAlign.center, + ), + ), + ); + } + + void setHidden(bool value) { + if (hover == value) return; + setState(() { + hover = value; + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart new file mode 100644 index 0000000000..d09c251420 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart @@ -0,0 +1,84 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg; +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/image.dart'; +import 'package:flutter/material.dart'; + +class EmojiPopover extends StatefulWidget { + final EditorState editorState; + final Node node; + final void Function(Emoji emoji) onEmojiChanged; + final VoidCallback removeIcon; + final bool showRemoveButton; + + const EmojiPopover({ + super.key, + required this.editorState, + required this.node, + required this.onEmojiChanged, + required this.removeIcon, + required this.showRemoveButton, + }); + + @override + State createState() => _EmojiPopoverState(); +} + +class _EmojiPopoverState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + if (widget.showRemoveButton) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Align( + alignment: Alignment.centerRight, + child: DeleteButton(onPressed: widget.removeIcon), + ), + ), + Expanded( + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + widget.onEmojiChanged(emoji); + }, + config: Config( + columns: 8, + emojiSizeMax: 28, + bgColor: Colors.transparent, + iconColor: Theme.of(context).iconTheme.color!, + iconColorSelected: Theme.of(context).colorScheme.onSurface, + selectedHoverColor: Theme.of(context).colorScheme.secondary, + progressIndicatorColor: Theme.of(context).iconTheme.color!, + buttonMode: ButtonMode.CUPERTINO, + initCategory: Category.RECENT, + ), + ), + ), + ], + ), + ); + } +} + +class DeleteButton extends StatelessWidget { + final VoidCallback onPressed; + const DeleteButton({required this.onPressed, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyButton( + onTap: () => onPressed, + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + ), + leftIcon: const FlowySvg(name: 'editor/delete'), + ); + } +} 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..c9242dc4e7 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/database_view/widgets/database_view_widget.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,19 +13,16 @@ 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, + this.configuration = const BlockComponentConfiguration(), }); + @override + final BlockComponentConfiguration configuration; + @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { final node = blockComponentContext.node; @@ -42,15 +35,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 +51,6 @@ class DatabaseBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -80,65 +68,24 @@ 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, - ), - ), - ); - - child = FocusScope( - skipTraversal: true, - onFocusChange: (value) { - if (value && keepEditorFocusNotifier.value == 0) { - context.read().selection = null; - } + builder: (viewPB) { + return DatabaseViewWidget( + key: ValueKey(viewPB.id), + view: viewPB, + ); }, - child: child, ); - if (!editorState.editable) { - child = IgnorePointer( + if (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/inline_database_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart index e31bd172c6..c07b0b47b3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart @@ -1,72 +1,90 @@ -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/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.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_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; SelectionMenuItem inlineGridMenuItem(DocumentBloc documentBloc) => SelectionMenuItem( - getName: LocaleKeys.document_slashMenu_grid_createANewGrid.tr, + name: LocaleKeys.document_slashMenu_grid_createANewGrid.tr(), icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.grid_s, + name: 'editor/grid', isSelected: onSelected, style: style, ), keywords: ['grid', 'database'], handler: (editorState, menuService, context) async { - // create the view inside current page - final parentViewId = documentBloc.documentId; - final value = await ViewBackendService.createView( + if (!documentBloc.view.hasParentViewId()) { + return; + } + + final parentViewId = documentBloc.view.parentViewId; + ViewBackendService.createView( parentViewId: parentViewId, + openAfterCreate: false, name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layoutType: ViewLayoutPB.Grid, + ).then( + (value) => value + .swap() + .map((r) => editorState.insertInlinePage(parentViewId, r)), ); - value.map((r) => editorState.insertInlinePage(parentViewId, r)); }, ); SelectionMenuItem inlineBoardMenuItem(DocumentBloc documentBloc) => SelectionMenuItem( - getName: LocaleKeys.document_slashMenu_board_createANewBoard.tr, + name: LocaleKeys.document_slashMenu_board_createANewBoard.tr(), icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.board_s, + name: 'editor/board', isSelected: onSelected, style: style, ), keywords: ['board', 'kanban', 'database'], handler: (editorState, menuService, context) async { - // create the view inside current page - final parentViewId = documentBloc.documentId; - final value = await ViewBackendService.createView( + if (!documentBloc.view.hasParentViewId()) { + return; + } + + final parentViewId = documentBloc.view.parentViewId; + ViewBackendService.createView( parentViewId: parentViewId, name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layoutType: ViewLayoutPB.Board, + ).then( + (value) => value + .swap() + .map((r) => editorState.insertInlinePage(parentViewId, r)), ); - value.map((r) => editorState.insertInlinePage(parentViewId, r)); }, ); SelectionMenuItem inlineCalendarMenuItem(DocumentBloc documentBloc) => SelectionMenuItem( - getName: LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr, + name: LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr(), icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.date_s, + name: 'editor/calendar', isSelected: onSelected, style: style, ), keywords: ['calendar', 'database'], handler: (editorState, menuService, context) async { - // create the view inside current page - final parentViewId = documentBloc.documentId; - final value = await ViewBackendService.createView( + if (!documentBloc.view.hasParentViewId()) { + return; + } + + final parentViewId = documentBloc.view.parentViewId; + ViewBackendService.createView( parentViewId: parentViewId, name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layoutType: ViewLayoutPB.Calendar, + ).then( + (value) => value + .swap() + .map((r) => editorState.insertInlinePage(parentViewId, r)), ); - value.map((r) => editorState.insertInlinePage(parentViewId, r)); }, ); 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 deleted file mode 100644 index 8f6c117833..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart +++ /dev/null @@ -1,71 +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/link_to_page_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.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'; - -// Document Reference - -SelectionMenuItem referencedDocumentMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_plugins_referencedDocument.tr, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.icon_document_s, - isSelected: onSelected, - style: style, - ), - keywords: ['page', 'notes', 'referenced page', 'referenced document'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Document, - ), -); - -// Database References - -SelectionMenuItem referencedGridMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_plugins_referencedGrid.tr, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.grid_s, - isSelected: onSelected, - style: style, - ), - keywords: ['referenced', 'grid', 'database'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Grid, - ), -); - -SelectionMenuItem referencedBoardMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_plugins_referencedBoard.tr, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.board_s, - isSelected: onSelected, - style: style, - ), - keywords: ['referenced', 'board', 'kanban'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Board, - ), -); - -SelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_plugins_referencedCalendar.tr, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.date_s, - isSelected: onSelected, - style: style, - ), - keywords: ['referenced', 'calendar', 'database'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Calendar, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.dart new file mode 100644 index 0000000000..ca82324813 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/generated/locale_keys.g.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_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +SelectionMenuItem referencedGridMenuItem = SelectionMenuItem( + name: LocaleKeys.document_plugins_referencedGrid.tr(), + icon: (editorState, onSelected, style) => SelectableSvgWidget( + name: 'editor/grid', + isSelected: onSelected, + style: style, + ), + keywords: ['referenced', 'grid', 'database'], + handler: (editorState, menuService, context) { + final container = Overlay.of(context); + showLinkToPageMenu( + container, + editorState, + menuService, + ViewLayoutPB.Grid, + ); + }, +); + +SelectionMenuItem referencedBoardMenuItem = SelectionMenuItem( + name: LocaleKeys.document_plugins_referencedBoard.tr(), + icon: (editorState, onSelected, style) => SelectableSvgWidget( + name: 'editor/board', + isSelected: onSelected, + style: style, + ), + keywords: ['referenced', 'board', 'kanban'], + handler: (editorState, menuService, context) { + final container = Overlay.of(context); + showLinkToPageMenu( + container, + editorState, + menuService, + ViewLayoutPB.Board, + ); + }, +); + +SelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem( + name: LocaleKeys.document_plugins_referencedCalendar.tr(), + icon: (editorState, onSelected, style) => SelectableSvgWidget( + name: 'editor/calendar', + isSelected: onSelected, + style: style, + ), + keywords: ['referenced', 'calendar', 'database'], + handler: (editorState, menuService, context) { + showLinkToPageMenu( + Overlay.of(context), + 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/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart new file mode 100644 index 0000000000..ac77f9951a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart @@ -0,0 +1,123 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'emoji_picker.dart'; + +SelectionMenuItem emojiMenuItem = SelectionMenuItem( + name: 'Emoji', + 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); + showEmojiPickerMenu( + container, + editorState, + menuService, + ); + }, +); + +void showEmojiPickerMenu( + OverlayState container, + EditorState editorState, + SelectionMenuService menuService, +) { + menuService.dismiss(); + + final alignment = menuService.alignment; + final offset = menuService.offset; + final top = alignment == Alignment.bottomLeft ? offset.dy : null; + final bottom = alignment == Alignment.topLeft ? offset.dy : null; + + keepEditorFocusNotifier.value += 1; + final emojiPickerMenuEntry = FullScreenOverlayEntry( + top: top, + bottom: bottom, + left: offset.dx, + dismissCallback: () => keepEditorFocusNotifier.value -= 1, + builder: (context) => Material( + child: Container( + width: 300, + height: 250, + padding: const EdgeInsets.all(4.0), + child: EmojiSelectionMenu( + onSubmitted: (emoji) { + editorState.insertTextAtCurrentSelection(emoji.emoji); + }, + onExit: () { + // close emoji panel + }, + ), + ), + ), + ).build(); + container.insert(emojiPickerMenuEntry); +} + +class EmojiSelectionMenu extends StatefulWidget { + const EmojiSelectionMenu({ + Key? key, + required this.onSubmitted, + required this.onExit, + }) : super(key: key); + + final void Function(Emoji 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 EmojiPicker( + onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji), + config: const Config( + columns: 7, + emojiSizeMax: 28, + bgColor: Colors.transparent, + iconColor: Colors.grey, + iconColorSelected: Color(0xff333333), + indicatorColor: Color(0xff333333), + progressIndicatorColor: Color(0xff333333), + buttonMode: ButtonMode.CUPERTINO, + initCategory: Category.RECENT, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart new file mode 100644 index 0000000000..fe4580db04 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart @@ -0,0 +1,4 @@ +export 'src/config.dart'; +export 'src/emoji_picker.dart'; +export 'src/emoji_picker_builder.dart'; +export 'src/models/emoji_model.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/config.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/config.dart new file mode 100644 index 0000000000..cbe6554df0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/config.dart @@ -0,0 +1,165 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'models/category_models.dart'; +import 'emoji_picker.dart'; + +/// Config for customizations +class Config { + /// Constructor + const Config( + {this.columns = 7, + this.emojiSizeMax = 32.0, + this.verticalSpacing = 0, + this.horizontalSpacing = 0, + this.initCategory = Category.RECENT, + this.bgColor = const Color(0xFFEBEFF2), + this.indicatorColor = Colors.blue, + this.iconColor = Colors.grey, + this.iconColorSelected = Colors.blue, + this.progressIndicatorColor = Colors.blue, + this.backspaceColor = Colors.blue, + this.showRecentsTab = true, + this.recentsLimit = 28, + this.noRecentsText = 'No Recents', + this.noRecentsStyle = + const TextStyle(fontSize: 20, color: Colors.black26), + this.tabIndicatorAnimDuration = kTabScrollDuration, + this.categoryIcons = const CategoryIcons(), + this.buttonMode = ButtonMode.MATERIAL,}); + + /// Number of emojis per row + final int columns; + + /// Width and height the emoji will be maximal displayed + /// Can be smaller due to screen size and amount of columns + final double emojiSizeMax; + + /// Vertical spacing between emojis + final double verticalSpacing; + + /// Horizontal spacing between emojis + final double horizontalSpacing; + + /// The initial [Category] that will be selected + /// This [Category] will have its button in the bottombar darkened + final Category initCategory; + + /// The background color of the Widget + final Color bgColor; + + /// The color of the category indicator + final Color indicatorColor; + + /// The color of the category icons + final Color iconColor; + + /// The color of the category icon when selected + final Color iconColorSelected; + + /// The color of the loading indicator during initialization + final Color progressIndicatorColor; + + /// The color of the backspace icon button + final Color backspaceColor; + + /// Show extra tab with recently used emoji + final bool showRecentsTab; + + /// Limit of recently used emoji that will be saved + final int recentsLimit; + + /// The text to be displayed if no recent emojis to display + final String noRecentsText; + + /// The text style for [noRecentsText] + final TextStyle noRecentsStyle; + + /// Duration of tab indicator to animate to next category + final Duration tabIndicatorAnimDuration; + + /// Determines the icon to display for each [Category] + final CategoryIcons categoryIcons; + + /// Change between Material and Cupertino button style + final ButtonMode buttonMode; + + /// Get Emoji size based on properties and screen width + double getEmojiSize(double width) { + final maxSize = width / columns; + return min(maxSize, emojiSizeMax); + } + + /// Returns the icon for the category + IconData getIconForCategory(Category category) { + switch (category) { + case Category.RECENT: + return categoryIcons.recentIcon; + case Category.SMILEYS: + return categoryIcons.smileyIcon; + case Category.ANIMALS: + return categoryIcons.animalIcon; + case Category.FOODS: + return categoryIcons.foodIcon; + case Category.TRAVEL: + return categoryIcons.travelIcon; + case Category.ACTIVITIES: + return categoryIcons.activityIcon; + case Category.OBJECTS: + return categoryIcons.objectIcon; + case Category.SYMBOLS: + return categoryIcons.symbolIcon; + case Category.FLAGS: + return categoryIcons.flagIcon; + case Category.SEARCH: + return categoryIcons.searchIcon; + default: + throw Exception('Unsupported Category'); + } + } + + @override + bool operator ==(other) { + return (other is Config) && + other.columns == columns && + other.emojiSizeMax == emojiSizeMax && + other.verticalSpacing == verticalSpacing && + other.horizontalSpacing == horizontalSpacing && + other.initCategory == initCategory && + other.bgColor == bgColor && + other.indicatorColor == indicatorColor && + other.iconColor == iconColor && + other.iconColorSelected == iconColorSelected && + other.progressIndicatorColor == progressIndicatorColor && + other.backspaceColor == backspaceColor && + other.showRecentsTab == showRecentsTab && + other.recentsLimit == recentsLimit && + other.noRecentsText == noRecentsText && + other.noRecentsStyle == noRecentsStyle && + other.tabIndicatorAnimDuration == tabIndicatorAnimDuration && + other.categoryIcons == categoryIcons && + other.buttonMode == buttonMode; + } + + @override + int get hashCode => + columns.hashCode ^ + emojiSizeMax.hashCode ^ + verticalSpacing.hashCode ^ + horizontalSpacing.hashCode ^ + initCategory.hashCode ^ + bgColor.hashCode ^ + indicatorColor.hashCode ^ + iconColor.hashCode ^ + iconColorSelected.hashCode ^ + progressIndicatorColor.hashCode ^ + backspaceColor.hashCode ^ + showRecentsTab.hashCode ^ + recentsLimit.hashCode ^ + noRecentsText.hashCode ^ + noRecentsStyle.hashCode ^ + tabIndicatorAnimDuration.hashCode ^ + categoryIcons.hashCode ^ + buttonMode.hashCode; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/default_emoji_picker_view.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/default_emoji_picker_view.dart new file mode 100644 index 0000000000..4bd3bcde7d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/default_emoji_picker_view.dart @@ -0,0 +1,293 @@ +import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; +import 'package:flutter/material.dart'; + +import 'config.dart'; +import 'emoji_picker.dart'; +import 'emoji_picker_builder.dart'; +import 'emoji_view_state.dart'; +import 'models/category_models.dart'; +import 'models/emoji_model.dart'; + +class DefaultEmojiPickerView extends EmojiPickerBuilder { + const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key}) + : super(config, state, key: key); + + @override + DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState(); +} + +class DefaultEmojiPickerViewState extends State + with TickerProviderStateMixin { + PageController? _pageController; + TabController? _tabController; + final TextEditingController _emojiController = TextEditingController(); + final FocusNode _emojiFocusNode = FocusNode(); + CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, []); + + @override + void initState() { + var initCategory = widget.state.categoryEmoji.indexWhere( + (element) => element.category == widget.config.initCategory,); + if (initCategory == -1) { + initCategory = 0; + } + _tabController = TabController( + initialIndex: initCategory, + length: widget.state.categoryEmoji.length, + vsync: this,); + _pageController = PageController(initialPage: initCategory); + _emojiFocusNode.requestFocus(); + + _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.categoryEmoji) { + searchEmojiList.emoji.addAll( + element.emoji.where((item) { + return item.name.toLowerCase().contains(query); + }).toList(), + ); + } + } + setState(() {}); + }); + super.initState(); + } + + @override + void dispose() { + _emojiController.dispose(); + _emojiFocusNode.dispose(); + super.dispose(); + } + + 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!(); + },), + ); + } + return Container(); + } + + bool isEmojiSearching() { + final bool result = + searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; + + return result; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final emojiSize = widget.config.getEmojiSize(constraints.maxWidth); + + return Container( + color: widget.config.bgColor, + padding: const EdgeInsets.all(5.0), + child: Column( + children: [ + SizedBox( + height: 25.0, + child: TextField( + controller: _emojiController, + focusNode: _emojiFocusNode, + autofocus: true, + style: const TextStyle(fontSize: 14.0), + cursorWidth: 1.0, + cursorColor: Colors.black, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 5.0), + hintText: "Search emoji", + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(4.0), + borderSide: const BorderSide(), + gapPadding: 0.0, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4.0), + borderSide: const BorderSide(), + gapPadding: 0.0, + ), + filled: true, + fillColor: Colors.white, + hoverColor: Colors.white, + ), + ), + ), + Row( + children: [ + Expanded( + child: TabBar( + labelColor: widget.config.iconColorSelected, + unselectedLabelColor: widget.config.iconColor, + controller: isEmojiSearching() + ? TabController(length: 1, vsync: this) + : _tabController, + labelPadding: EdgeInsets.zero, + indicatorColor: widget.config.indicatorColor, + padding: const EdgeInsets.symmetric(vertical: 5.0), + indicator: BoxDecoration( + border: Border.all(color: Colors.transparent), + borderRadius: BorderRadius.circular(4.0), + color: Colors.grey.withOpacity(0.5), + ), + onTap: (index) { + _pageController!.animateToPage( + index, + duration: widget.config.tabIndicatorAnimDuration, + curve: Curves.ease, + ); + }, + tabs: isEmojiSearching() + ? [_buildCategory(Category.SEARCH, emojiSize)] + : widget.state.categoryEmoji + .asMap() + .entries + .map((item) => _buildCategory( + item.value.category, emojiSize,),) + .toList(), + ), + ), + _buildBackspaceButton(), + ], + ), + Flexible( + child: PageView.builder( + itemCount: searchEmojiList.emoji.isNotEmpty + ? 1 + : widget.state.categoryEmoji.length, + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + // onPageChanged: (index) { + // _tabController!.animateTo( + // index, + // duration: widget.config.tabIndicatorAnimDuration, + // ); + // }, + itemBuilder: (context, index) { + final CategoryEmoji catEmoji = isEmojiSearching() + ? searchEmojiList + : widget.state.categoryEmoji[index]; + return _buildPage(emojiSize, catEmoji); + }, + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildCategory(Category category, double categorySize) { + return Tab( + height: categorySize, + child: Icon( + widget.config.getIconForCategory(category), + size: categorySize / 1.3, + ), + ); + } + + Widget _buildButtonWidget( + {required VoidCallback onPressed, required Widget child,}) { + if (widget.config.buttonMode == ButtonMode.MATERIAL) { + return InkWell( + onTap: onPressed, + child: child, + ); + } + return GestureDetector( + onTap: onPressed, + child: child, + ); + } + + Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) { + // Display notice if recent has no entries yet + final scrollController = ScrollController(); + + if (categoryEmoji.category == Category.RECENT && + categoryEmoji.emoji.isEmpty) { + return _buildNoRecent(); + } else if (categoryEmoji.category == Category.SEARCH && + categoryEmoji.emoji.isEmpty) { + return const Center(child: Text("No Emoji Found")); + } + // Build page normally + return ScrollbarListStack( + axis: Axis.vertical, + controller: scrollController, + barSize: 4.0, + scrollbarPadding: const EdgeInsets.symmetric(horizontal: 5.0), + handleColor: const Color(0xffDFE0E0), + trackColor: const Color(0xffDFE0E0), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: GridView.builder( + controller: scrollController, + padding: const EdgeInsets.all(0), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.config.columns, + mainAxisSpacing: widget.config.verticalSpacing, + crossAxisSpacing: widget.config.horizontalSpacing, + ), + itemCount: categoryEmoji.emoji.length, + itemBuilder: (context, index) { + final item = categoryEmoji.emoji[index]; + return _buildEmoji(emojiSize, categoryEmoji, item); + }, + cacheExtent: 10, + ), + ), + ); + } + + Widget _buildEmoji( + double emojiSize, + CategoryEmoji categoryEmoji, + Emoji emoji, + ) { + return _buildButtonWidget( + onPressed: () { + widget.state.onEmojiSelected(categoryEmoji.category, emoji); + }, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + emoji.emoji, + textScaleFactor: 1.0, + style: TextStyle( + fontSize: emojiSize, + backgroundColor: Colors.transparent, + ), + ), + ),); + } + + Widget _buildNoRecent() { + return Center( + child: Text( + widget.config.noRecentsText, + style: widget.config.noRecentsStyle, + textAlign: TextAlign.center, + ),); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_lists.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_lists.dart new file mode 100644 index 0000000000..6cb3681206 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_lists.dart @@ -0,0 +1,3223 @@ +// Copyright information +// File originally from https://github.com/JeffG05/emoji_picker + +// import 'emoji.dart'; + +// final List> temp = [smileys, animals, foods, activities, travel, objects, symbols, flags]; + +// final List emojiSearchList = temp +// .map((element) { +// return element.entries.map((entry) => Emoji(entry.key, entry.value)).toList(); +// }) +// .toList() +// .first; + +/// Map of all possible emojis along with their names in [Category.SMILEYS] +final Map smileys = Map.fromIterables([ + 'Grinning Face', + 'Grinning Face With Big Eyes', + 'Grinning Face With Smiling Eyes', + 'Beaming Face With Smiling Eyes', + 'Grinning Squinting Face', + 'Grinning Face With Sweat', + 'Rolling on the Floor Laughing', + 'Face With Tears of Joy', + 'Slightly Smiling Face', + 'Upside-Down Face', + 'Winking Face', + 'Smiling Face With Smiling Eyes', + 'Smiling Face With Halo', + 'Smiling Face With Hearts', + 'Smiling Face With Heart-Eyes', + 'Star-Struck', + 'Face Blowing a Kiss', + 'Kissing Face', + 'Smiling Face', + 'Kissing Face With Closed Eyes', + 'Kissing Face With Smiling Eyes', + 'Face Savoring Food', + 'Face With Tongue', + 'Winking Face With Tongue', + 'Zany Face', + 'Squinting Face With Tongue', + 'Money-Mouth Face', + 'Hugging Face', + 'Face With Hand Over Mouth', + 'Shushing Face', + 'Thinking Face', + 'Zipper-Mouth Face', + 'Face With Raised Eyebrow', + 'Neutral Face', + 'Expressionless Face', + 'Face Without Mouth', + 'Smirking Face', + 'Unamused Face', + 'Face With Rolling Eyes', + 'Grimacing Face', + 'Lying Face', + 'Relieved Face', + 'Pensive Face', + 'Sleepy Face', + 'Drooling Face', + 'Sleeping Face', + 'Face With Medical Mask', + 'Face With Thermometer', + 'Face With Head-Bandage', + 'Nauseated Face', + 'Face Vomiting', + 'Sneezing Face', + 'Hot Face', + 'Cold Face', + 'Woozy Face', + 'Dizzy Face', + 'Exploding Head', + 'Cowboy Hat Face', + 'Partying Face', + 'Smiling Face With Sunglasses', + 'Nerd Face', + 'Face With Monocle', + 'Confused Face', + 'Worried Face', + 'Slightly Frowning Face', + 'Frowning Face', + 'Face With Open Mouth', + 'Hushed Face', + 'Astonished Face', + 'Flushed Face', + 'Pleading Face', + 'Frowning Face With Open Mouth', + 'Anguished Face', + 'Fearful Face', + 'Anxious Face With Sweat', + 'Sad but Relieved Face', + 'Crying Face', + 'Loudly Crying Face', + 'Face Screaming in Fear', + 'Confounded Face', + 'Persevering Face', + 'Disappointed Face', + 'Downcast Face With Sweat', + 'Weary Face', + 'Tired Face', + 'Face With Steam From Nose', + 'Pouting Face', + 'Angry Face', + 'Face With Symbols on Mouth', + 'Smiling Face With Horns', + 'Angry Face With Horns', + 'Skull', + 'Skull and Crossbones', + 'Pile of Poo', + 'Clown Face', + 'Ogre', + 'Goblin', + 'Ghost', + 'Alien', + 'Alien Monster', + 'Robot Face', + 'Grinning Cat Face', + 'Grinning Cat Face With Smiling Eyes', + 'Cat Face With Tears of Joy', + 'Smiling Cat Face With Heart-Eyes', + 'Cat Face With Wry Smile', + 'Kissing Cat Face', + 'Weary Cat Face', + 'Crying Cat Face', + 'Pouting Cat Face', + 'Kiss Mark', + 'Waving Hand', + 'Raised Back of Hand', + 'Hand With Fingers Splayed', + 'Raised Hand', + 'Vulcan Salute', + 'OK Hand', + 'Victory Hand', + 'Crossed Fingers', + 'Love-You Gesture', + 'Sign of the Horns', + 'Call Me Hand', + 'Backhand Index Pointing Left', + 'Backhand Index Pointing Right', + 'Backhand Index Pointing Up', + 'Middle Finger', + 'Backhand Index Pointing Down', + 'Index Pointing Up', + 'Thumbs Up', + 'Thumbs Down', + 'Raised Fist', + 'Oncoming Fist', + 'Left-Facing Fist', + 'Right-Facing Fist', + 'Clapping Hands', + 'Raising Hands', + 'Open Hands', + 'Palms Up Together', + 'Handshake', + 'Folded Hands', + 'Writing Hand', + 'Nail Polish', + 'Selfie', + 'Flexed Biceps', + 'Leg', + 'Foot', + 'Ear', + 'Nose', + 'Brain', + 'Tooth', + 'Bone', + 'Eyes', + 'Eye', + 'Tongue', + 'Mouth', + 'Baby', + 'Child', + 'Boy', + 'Girl', + 'Person', + 'Man', + 'Man: Beard', + 'Man: Blond Hair', + 'Man: Red Hair', + 'Man: Curly Hair', + 'Man: White Hair', + 'Man: Bald', + 'Woman', + 'Woman: Blond Hair', + 'Woman: Red Hair', + 'Woman: Curly Hair', + 'Woman: White Hair', + 'Woman: Bald', + 'Older Person', + 'Old Man', + 'Old Woman', + 'Man Frowning', + 'Woman Frowning', + 'Man Pouting', + 'Woman Pouting', + 'Man Gesturing No', + 'Woman Gesturing No', + 'Man Gesturing OK', + 'Woman Gesturing OK', + 'Man Tipping Hand', + 'Woman Tipping Hand', + 'Man Raising Hand', + 'Woman Raising Hand', + 'Man Bowing', + 'Woman Bowing', + 'Man Facepalming', + 'Woman Facepalming', + 'Man Shrugging', + 'Woman Shrugging', + 'Man Health Worker', + 'Woman Health Worker', + 'Man Student', + 'Woman Student', + 'Man Teacher', + 'Woman Teacher', + 'Man Judge', + 'Woman Judge', + 'Man Farmer', + 'Woman Farmer', + 'Man Cook', + 'Woman Cook', + 'Man Mechanic', + 'Woman Mechanic', + 'Man Factory Worker', + 'Woman Factory Worker', + 'Man Office Worker', + 'Woman Office Worker', + 'Man Scientist', + 'Woman Scientist', + 'Man Technologist', + 'Woman Technologist', + 'Man Singer', + 'Woman Singer', + 'Man Artist', + 'Woman Artist', + 'Man Pilot', + 'Woman Pilot', + 'Man Astronaut', + 'Woman Astronaut', + 'Man Firefighter', + 'Woman Firefighter', + 'Man Police Officer', + 'Woman Police Officer', + 'Man Detective', + 'Woman Detective', + 'Man Guard', + 'Woman Guard', + 'Man Construction Worker', + 'Woman Construction Worker', + 'Prince', + 'Princess', + 'Man Wearing Turban', + 'Woman Wearing Turban', + 'Man With Chinese Cap', + 'Woman With Headscarf', + 'Man in Tuxedo', + 'Bride With Veil', + 'Pregnant Woman', + 'Breast-Feeding', + 'Baby Angel', + 'Santa Claus', + 'Mrs. Claus', + 'Man Superhero', + 'Woman Superhero', + 'Man Supervillain', + 'Woman Supervillain', + 'Man Mage', + 'Woman Mage', + 'Man Fairy', + 'Woman Fairy', + 'Man Vampire', + 'Woman Vampire', + 'Merman', + 'Mermaid', + 'Man Elf', + 'Woman Elf', + 'Man Genie', + 'Woman Genie', + 'Man Zombie', + 'Woman Zombie', + 'Man Getting Massage', + 'Woman Getting Massage', + 'Man Getting Haircut', + 'Woman Getting Haircut', + 'Man Walking', + 'Woman Walking', + 'Man Running', + 'Woman Running', + 'Woman Dancing', + 'Man Dancing', + 'Man in Suit Levitating', + 'Men With Bunny Ears', + 'Women With Bunny Ears', + 'Man in Steamy Room', + 'Woman in Steamy Room', + 'Person in Lotus Position', + 'Women Holding Hands', + 'Woman and Man Holding Hands', + 'Men Holding Hands', + 'Kiss', + 'Kiss: Man, Man', + 'Kiss: Woman, Woman', + 'Couple With Heart', + 'Couple With Heart: Man, Man', + 'Couple With Heart: Woman, Woman', + 'Family', + 'Family: Man, Woman, Boy', + 'Family: Man, Woman, Girl', + 'Family: Man, Woman, Girl, Boy', + 'Family: Man, Woman, Boy, Boy', + 'Family: Man, Woman, Girl, Girl', + 'Family: Man, Man, Boy', + 'Family: Man, Man, Girl', + 'Family: Man, Man, Girl, Boy', + 'Family: Man, Man, Boy, Boy', + 'Family: Man, Man, Girl, Girl', + 'Family: Woman, Woman, Boy', + 'Family: Woman, Woman, Girl', + 'Family: Woman, Woman, Girl, Boy', + 'Family: Woman, Woman, Boy, Boy', + 'Family: Woman, Woman, Girl, Girl', + 'Family: Man, Boy', + 'Family: Man, Boy, Boy', + 'Family: Man, Girl', + 'Family: Man, Girl, Boy', + 'Family: Man, Girl, Girl', + 'Family: Woman, Boy', + 'Family: Woman, Boy, Boy', + 'Family: Woman, Girl', + 'Family: Woman, Girl, Boy', + 'Family: Woman, Girl, Girl', + 'Speaking Head', + 'Bust in Silhouette', + 'Busts in Silhouette', + 'Footprints', + 'Luggage', + 'Closed Umbrella', + 'Umbrella', + 'Thread', + 'Yarn', + 'Glasses', + 'Sunglasses', + 'Goggles', + 'Lab Coat', + 'Necktie', + 'T-Shirt', + 'Jeans', + 'Scarf', + 'Gloves', + 'Coat', + 'Socks', + 'Dress', + 'Kimono', + 'Bikini', + 'Woman’s Clothes', + 'Purse', + 'Handbag', + 'Clutch Bag', + 'Backpack', + 'Man’s Shoe', + 'Running Shoe', + 'Hiking Boot', + 'Flat Shoe', + 'High-Heeled Shoe', + 'Woman’s Sandal', + 'Woman’s Boot', + 'Crown', + 'Woman’s Hat', + 'Top Hat', + 'Graduation Cap', + 'Billed Cap', + 'Rescue Worker’s Helmet', + 'Lipstick', + 'Ring', + 'Briefcase' +], [ + '😀', + '😃', + '😄', + '😁', + '😆', + '😅', + '🤣', + '😂', + '🙂', + '🙃', + '😉', + '😊', + '😇', + '🥰', + '😍', + '🤩', + '😘', + '😗', + '☺', + '😚', + '😙', + '😋', + '😛', + '😜', + '🤪', + '😝', + '🤑', + '🤗', + '🤭', + '🤫', + '🤔', + '🤐', + '🤨', + '😐', + '😑', + '😶', + '😏', + '😒', + '🙄', + '😬', + '🤥', + '😌', + '😔', + '😪', + '🤤', + '😴', + '😷', + '🤒', + '🤕', + '🤢', + '🤮', + '🤧', + '🥵', + '🥶', + '🥴', + '😵', + '🤯', + '🤠', + '🥳', + '😎', + '🤓', + '🧐', + '😕', + '😟', + '🙁', + '☹️', + '😮', + '😯', + '😲', + '😳', + '🥺', + '😦', + '😧', + '😨', + '😰', + '😥', + '😢', + '😭', + '😱', + '😖', + '😣', + '😞', + '😓', + '😩', + '😫', + '😤', + '😡', + '😠', + '🤬', + '😈', + '👿', + '💀', + '☠', + '💩', + '🤡', + '👹', + '👺', + '👻', + '👽', + '👾', + '🤖', + '😺', + '😸', + '😹', + '😻', + '😼', + '😽', + '🙀', + '😿', + '😾', + '💋', + '👋', + '🤚', + '🖐', + '✋', + '🖖', + '👌', + '✌', + '🤞', + '🤟', + '🤘', + '🤙', + '👈', + '👉', + '👆', + '🖕', + '👇', + '☝', + '👍', + '👎', + '✊', + '👊', + '🤛', + '🤜', + '👏', + '🙌', + '👐', + '🤲', + '🤝', + '🙏', + '✍', + '💅', + '🤳', + '💪', + '🦵', + '🦶', + '👂', + '👃', + '🧠', + '🦷', + '🦴', + '👀', + '👁', + '👅', + '👄', + '👶', + '🧒', + '👦', + '👧', + '🧑', + '👨', + '🧔', + '👱', + '👨‍🦰', + '👨‍🦱', + '👨‍🦳', + '👨‍🦲', + '👩', + '👱', + '👩‍🦰', + '👩‍🦱', + '👩‍🦳', + '👩‍🦲', + '🧓', + '👴', + '👵', + '🙍', + '🙍', + '🙎', + '🙎', + '🙅', + '🙅', + '🙆', + '🙆', + '💁', + '💁', + '🙋', + '🙋', + '🙇', + '🙇', + '🤦', + '🤦', + '🤷', + '🤷', + '👨‍⚕️', + '👩‍⚕️', + '👨‍🎓', + '👩‍🎓', + '👨‍🏫', + '👩‍🏫', + '👨‍⚖️', + '👩‍⚖️', + '👨‍🌾', + '👩‍🌾', + '👨‍🍳', + '👩‍🍳', + '👨‍🔧', + '👩‍🔧', + '👨‍🏭', + '👩‍🏭', + '👨‍💼', + '👩‍💼', + '👨‍🔬', + '👩‍🔬', + '👨‍💻', + '👩‍💻', + '👨‍🎤', + '👩‍🎤', + '👨‍🎨', + '👩‍🎨', + '👨‍✈️', + '👩‍✈️', + '👨‍🚀', + '👩‍🚀', + '👨‍🚒', + '👩‍🚒', + '👮', + '👮', + '🕵️', + '🕵️', + '💂', + '💂', + '👷', + '👷', + '🤴', + '👸', + '👳', + '👳', + '👲', + '🧕', + '🤵', + '👰', + '🤰', + '🤱', + '👼', + '🎅', + '🤶', + '🦸', + '🦸', + '🦹', + '🦹', + '🧙', + '🧙', + '🧚', + '🧚', + '🧛', + '🧛', + '🧜', + '🧜', + '🧝', + '🧝', + '🧞', + '🧞', + '🧟', + '🧟', + '💆', + '💆', + '💇', + '💇', + '🚶', + '🚶', + '🏃', + '🏃', + '💃', + '🕺', + '🕴', + '👯', + '👯', + '🧖', + '🧖', + '🧘', + '👭', + '👫', + '👬', + '💏', + '👨‍❤️‍💋‍👨', + '👩‍❤️‍💋‍👩', + '💑', + '👨‍❤️‍👨', + '👩‍❤️‍👩', + '👪', + '👨‍👩‍👦', + '👨‍👩‍👧', + '👨‍👩‍👧‍👦', + '👨‍👩‍👦‍👦', + '👨‍👩‍👧‍👧', + '👨‍👨‍👦', + '👨‍👨‍👧', + '👨‍👨‍👧‍👦', + '👨‍👨‍👦‍👦', + '👨‍👨‍👧‍👧', + '👩‍👩‍👦', + '👩‍👩‍👧', + '👩‍👩‍👧‍👦', + '👩‍👩‍👦‍👦', + '👩‍👩‍👧‍👧', + '👨‍👦', + '👨‍👦‍👦', + '👨‍👧', + '👨‍👧‍👦', + '👨‍👧‍👧', + '👩‍👦', + '👩‍👦‍👦', + '👩‍👧', + '👩‍👧‍👦', + '👩‍👧‍👧', + '🗣', + '👤', + '👥', + '👣', + '🧳', + '🌂', + '☂', + '🧵', + '🧶', + '👓', + '🕶', + '🥽', + '🥼', + '👔', + '👕', + '👖', + '🧣', + '🧤', + '🧥', + '🧦', + '👗', + '👘', + '👙', + '👚', + '👛', + '👜', + '👝', + '🎒', + '👞', + '👟', + '🥾', + '🥿', + '👠', + '👡', + '👢', + '👑', + '👒', + '🎩', + '🎓', + '🧢', + '⛑', + '💄', + '💍', + '💼' +]); + +/// Map of all possible emojis along with their names in [Category.ANIMALS] +final Map animals = Map.fromIterables([ + 'Dog Face', + 'Cat Face', + 'Mouse Face', + 'Hamster Face', + 'Rabbit Face', + 'Fox Face', + 'Bear Face', + 'Panda Face', + 'Koala Face', + 'Tiger Face', + 'Lion Face', + 'Cow Face', + 'Pig Face', + 'Pig Nose', + 'Frog Face', + 'Monkey Face', + 'See-No-Evil Monkey', + 'Hear-No-Evil Monkey', + 'Speak-No-Evil Monkey', + 'Monkey', + 'Collision', + 'Dizzy', + 'Sweat Droplets', + 'Dashing Away', + 'Gorilla', + 'Dog', + 'Poodle', + 'Wolf Face', + 'Raccoon', + 'Cat', + 'Tiger', + 'Leopard', + 'Horse Face', + 'Horse', + 'Unicorn Face', + 'Zebra', + 'Ox', + 'Water Buffalo', + 'Cow', + 'Pig', + 'Boar', + 'Ram', + 'Ewe', + 'Goat', + 'Camel', + 'Two-Hump Camel', + 'Llama', + 'Giraffe', + 'Elephant', + 'Rhinoceros', + 'Hippopotamus', + 'Mouse', + 'Rat', + 'Rabbit', + 'Chipmunk', + 'Hedgehog', + 'Bat', + 'Kangaroo', + 'Badger', + 'Paw Prints', + 'Turkey', + 'Chicken', + 'Rooster', + 'Hatching Chick', + 'Baby Chick', + 'Front-Facing Baby Chick', + 'Bird', + 'Penguin', + 'Dove', + 'Eagle', + 'Duck', + 'Swan', + 'Owl', + 'Peacock', + 'Parrot', + 'Crocodile', + 'Turtle', + 'Lizard', + 'Snake', + 'Dragon Face', + 'Dragon', + 'Sauropod', + 'T-Rex', + 'Spouting Whale', + 'Whale', + 'Dolphin', + 'Fish', + 'Tropical Fish', + 'Blowfish', + 'Shark', + 'Octopus', + 'Spiral Shell', + 'Snail', + 'Butterfly', + 'Bug', + 'Ant', + 'Honeybee', + 'Lady Beetle', + 'Cricket', + 'Spider', + 'Spider Web', + 'Scorpion', + 'Mosquito', + 'Microbe', + 'Bouquet', + 'Cherry Blossom', + 'White Flower', + 'Rosette', + 'Rose', + 'Wilted Flower', + 'Hibiscus', + 'Sunflower', + 'Blossom', + 'Tulip', + 'Seedling', + 'Evergreen Tree', + 'Deciduous Tree', + 'Palm Tree', + 'Cactus', + 'Sheaf of Rice', + 'Herb', + 'Shamrock', + 'Four Leaf Clover', + 'Maple Leaf', + 'Fallen Leaf', + 'Leaf Fluttering in Wind', + 'Mushroom', + 'Chestnut', + 'Crab', + 'Lobster', + 'Shrimp', + 'Squid', + 'Globe Showing Europe-Africa', + 'Globe Showing Americas', + 'Globe Showing Asia-Australia', + 'Globe With Meridians', + 'New Moon', + 'Waxing Crescent Moon', + 'First Quarter Moon', + 'Waxing Gibbous Moon', + 'Full Moon', + 'Waning Gibbous Moon', + 'Last Quarter Moon', + 'Waning Crescent Moon', + 'Crescent Moon', + 'New Moon Face', + 'First Quarter Moon Face', + 'Last Quarter Moon Face', + 'Sun', + 'Full Moon Face', + 'Sun With Face', + 'Star', + 'Glowing Star', + 'Shooting Star', + 'Cloud', + 'Sun Behind Cloud', + 'Cloud With Lightning and Rain', + 'Sun Behind Small Cloud', + 'Sun Behind Large Cloud', + 'Sun Behind Rain Cloud', + 'Cloud With Rain', + 'Cloud With Snow', + 'Cloud With Lightning', + 'Tornado', + 'Fog', + 'Wind Face', + 'Rainbow', + 'Umbrella', + 'Umbrella With Rain Drops', + 'High Voltage', + 'Snowflake', + 'Snowman Without Snow', + 'Snowman', + 'Comet', + 'Fire', + 'Droplet', + 'Water Wave', + 'Christmas Tree', + 'Sparkles', + 'Tanabata Tree', + 'Pine Decoration' +], [ + '🐶', + '🐱', + '🐭', + '🐹', + '🐰', + '🦊', + '🐻', + '🐼', + '🐨', + '🐯', + '🦁', + '🐮', + '🐷', + '🐽', + '🐸', + '🐵', + '🙈', + '🙉', + '🙊', + '🐒', + '💥', + '💫', + '💦', + '💨', + '🦍', + '🐕', + '🐩', + '🐺', + '🦝', + '🐈', + '🐅', + '🐆', + '🐴', + '🐎', + '🦄', + '🦓', + '🐂', + '🐃', + '🐄', + '🐖', + '🐗', + '🐏', + '🐑', + '🐐', + '🐪', + '🐫', + '🦙', + '🦒', + '🐘', + '🦏', + '🦛', + '🐁', + '🐀', + '🐇', + '🐿', + '🦔', + '🦇', + '🦘', + '🦡', + '🐾', + '🦃', + '🐔', + '🐓', + '🐣', + '🐤', + '🐥', + '🐦', + '🐧', + '🕊', + '🦅', + '🦆', + '🦢', + '🦉', + '🦚', + '🦜', + '🐊', + '🐢', + '🦎', + '🐍', + '🐲', + '🐉', + '🦕', + '🦖', + '🐳', + '🐋', + '🐬', + '🐟', + '🐠', + '🐡', + '🦈', + '🐙', + '🐚', + '🐌', + '🦋', + '🐛', + '🐜', + '🐝', + '🐞', + '🦗', + '🕷', + '🕸', + '🦂', + '🦟', + '🦠', + '💐', + '🌸', + '💮', + '🏵', + '🌹', + '🥀', + '🌺', + '🌻', + '🌼', + '🌷', + '🌱', + '🌲', + '🌳', + '🌴', + '🌵', + '🌾', + '🌿', + '☘', + '🍀', + '🍁', + '🍂', + '🍃', + '🍄', + '🌰', + '🦀', + '🦞', + '🦐', + '🦑', + '🌍', + '🌎', + '🌏', + '🌐', + '🌑', + '🌒', + '🌓', + '🌔', + '🌕', + '🌖', + '🌗', + '🌘', + '🌙', + '🌚', + '🌛', + '🌜', + '☀', + '🌝', + '🌞', + '⭐', + '🌟', + '🌠', + '☁', + '⛅', + '⛈', + '🌤', + '🌥', + '🌦', + '🌧', + '🌨', + '🌩', + '🌪', + '🌫', + '🌬', + '🌈', + '☂', + '☔', + '⚡', + '❄', + '☃', + '⛄', + '☄', + '🔥', + '💧', + '🌊', + '🎄', + '✨', + '🎋', + '🎍' +]); + +/// Map of all possible emojis along with their names in [Category.FOODS] +final Map foods = Map.fromIterables([ + 'Grapes', + 'Melon', + 'Watermelon', + 'Tangerine', + 'Lemon', + 'Banana', + 'Pineapple', + 'Mango', + 'Red Apple', + 'Green Apple', + 'Pear', + 'Peach', + 'Cherries', + 'Strawberry', + 'Kiwi Fruit', + 'Tomato', + 'Coconut', + 'Avocado', + 'Eggplant', + 'Potato', + 'Carrot', + 'Ear of Corn', + 'Hot Pepper', + 'Cucumber', + 'Leafy Green', + 'Broccoli', + 'Mushroom', + 'Peanuts', + 'Chestnut', + 'Bread', + 'Croissant', + 'Baguette Bread', + 'Pretzel', + 'Bagel', + 'Pancakes', + 'Cheese Wedge', + 'Meat on Bone', + 'Poultry Leg', + 'Cut of Meat', + 'Bacon', + 'Hamburger', + 'French Fries', + 'Pizza', + 'Hot Dog', + 'Sandwich', + 'Taco', + 'Burrito', + 'Stuffed Flatbread', + 'Cooking', + 'Shallow Pan of Food', + 'Pot of Food', + 'Bowl With Spoon', + 'Green Salad', + 'Popcorn', + 'Salt', + 'Canned Food', + 'Bento Box', + 'Rice Cracker', + 'Rice Ball', + 'Cooked Rice', + 'Curry Rice', + 'Steaming Bowl', + 'Spaghetti', + 'Roasted Sweet Potato', + 'Oden', + 'Sushi', + 'Fried Shrimp', + 'Fish Cake With Swirl', + 'Moon Cake', + 'Dango', + 'Dumpling', + 'Fortune Cookie', + 'Takeout Box', + 'Soft Ice Cream', + 'Shaved Ice', + 'Ice Cream', + 'Doughnut', + 'Cookie', + 'Birthday Cake', + 'Shortcake', + 'Cupcake', + 'Pie', + 'Chocolate Bar', + 'Candy', + 'Lollipop', + 'Custard', + 'Honey Pot', + 'Baby Bottle', + 'Glass of Milk', + 'Hot Beverage', + 'Teacup Without Handle', + 'Sake', + 'Bottle With Popping Cork', + 'Wine Glass', + 'Cocktail Glass', + 'Tropical Drink', + 'Beer Mug', + 'Clinking Beer Mugs', + 'Clinking Glasses', + 'Tumbler Glass', + 'Cup With Straw', + 'Chopsticks', + 'Fork and Knife With Plate', + 'Fork and Knife', + 'Spoon' +], [ + '🍇', + '🍈', + '🍉', + '🍊', + '🍋', + '🍌', + '🍍', + '🥭', + '🍎', + '🍏', + '🍐', + '🍑', + '🍒', + '🍓', + '🥝', + '🍅', + '🥥', + '🥑', + '🍆', + '🥔', + '🥕', + '🌽', + '🌶', + '🥒', + '🥬', + '🥦', + '🍄', + '🥜', + '🌰', + '🍞', + '🥐', + '🥖', + '🥨', + '🥯', + '🥞', + '🧀', + '🍖', + '🍗', + '🥩', + '🥓', + '🍔', + '🍟', + '🍕', + '🌭', + '🥪', + '🌮', + '🌯', + '🥙', + '🍳', + '🥘', + '🍲', + '🥣', + '🥗', + '🍿', + '🧂', + '🥫', + '🍱', + '🍘', + '🍙', + '🍚', + '🍛', + '🍜', + '🍝', + '🍠', + '🍢', + '🍣', + '🍤', + '🍥', + '🥮', + '🍡', + '🥟', + '🥠', + '🥡', + '🍦', + '🍧', + '🍨', + '🍩', + '🍪', + '🎂', + '🍰', + '🧁', + '🥧', + '🍫', + '🍬', + '🍭', + '🍮', + '🍯', + '🍼', + '🥛', + '☕', + '🍵', + '🍶', + '🍾', + '🍷', + '🍸', + '🍹', + '🍺', + '🍻', + '🥂', + '🥃', + '🥤', + '🥢', + '🍽', + '🍴', + '🥄' +]); + +/// Map of all possible emojis along with their names in [Category.TRAVEL] +final Map travel = Map.fromIterables([ + 'Person Rowing Boat', + 'Map of Japan', + 'Snow-Capped Mountain', + 'Mountain', + 'Volcano', + 'Mount Fuji', + 'Camping', + 'Beach With Umbrella', + 'Desert', + 'Desert Island', + 'National Park', + 'Stadium', + 'Classical Building', + 'Building Construction', + 'Houses', + 'Derelict House', + 'House', + 'House With Garden', + 'Office Building', + 'Japanese Post Office', + 'Post Office', + 'Hospital', + 'Bank', + 'Hotel', + 'Love Hotel', + 'Convenience Store', + 'School', + 'Department Store', + 'Factory', + 'Japanese Castle', + 'Castle', + 'Wedding', + 'Tokyo Tower', + 'Statue of Liberty', + 'Church', + 'Mosque', + 'Synagogue', + 'Shinto Shrine', + 'Kaaba', + 'Fountain', + 'Tent', + 'Foggy', + 'Night With Stars', + 'Cityscape', + 'Sunrise Over Mountains', + 'Sunrise', + 'Cityscape at Dusk', + 'Sunset', + 'Bridge at Night', + 'Carousel Horse', + 'Ferris Wheel', + 'Roller Coaster', + 'Locomotive', + 'Railway Car', + 'High-Speed Train', + 'Bullet Train', + 'Train', + 'Metro', + 'Light Rail', + 'Station', + 'Tram', + 'Monorail', + 'Mountain Railway', + 'Tram Car', + 'Bus', + 'Oncoming Bus', + 'Trolleybus', + 'Minibus', + 'Ambulance', + 'Fire Engine', + 'Police Car', + 'Oncoming Police Car', + 'Taxi', + 'Oncoming Taxi', + 'Automobile', + 'Oncoming Automobile', + 'Delivery Truck', + 'Articulated Lorry', + 'Tractor', + 'Racing Car', + 'Motorcycle', + 'Motor Scooter', + 'Bicycle', + 'Kick Scooter', + 'Bus Stop', + 'Railway Track', + 'Fuel Pump', + 'Police Car Light', + 'Horizontal Traffic Light', + 'Vertical Traffic Light', + 'Construction', + 'Anchor', + 'Sailboat', + 'Speedboat', + 'Passenger Ship', + 'Ferry', + 'Motor Boat', + 'Ship', + 'Airplane', + 'Small Airplane', + 'Airplane Departure', + 'Airplane Arrival', + 'Seat', + 'Helicopter', + 'Suspension Railway', + 'Mountain Cableway', + 'Aerial Tramway', + 'Satellite', + 'Rocket', + 'Flying Saucer', + 'Shooting Star', + 'Milky Way', + 'Umbrella on Ground', + 'Fireworks', + 'Sparkler', + 'Moon Viewing Ceremony', + 'Yen Banknote', + 'Dollar Banknote', + 'Euro Banknote', + 'Pound Banknote', + 'Moai', + 'Passport Control', + 'Customs', + 'Baggage Claim', + 'Left Luggage' +], [ + '🚣', + '🗾', + '🏔', + '⛰', + '🌋', + '🗻', + '🏕', + '🏖', + '🏜', + '🏝', + '🏞', + '🏟', + '🏛', + '🏗', + '🏘', + '🏚', + '🏠', + '🏡', + '🏢', + '🏣', + '🏤', + '🏥', + '🏦', + '🏨', + '🏩', + '🏪', + '🏫', + '🏬', + '🏭', + '🏯', + '🏰', + '💒', + '🗼', + '🗽', + '⛪', + '🕌', + '🕍', + '⛩', + '🕋', + '⛲', + '⛺', + '🌁', + '🌃', + '🏙', + '🌄', + '🌅', + '🌆', + '🌇', + '🌉', + '🎠', + '🎡', + '🎢', + '🚂', + '🚃', + '🚄', + '🚅', + '🚆', + '🚇', + '🚈', + '🚉', + '🚊', + '🚝', + '🚞', + '🚋', + '🚌', + '🚍', + '🚎', + '🚐', + '🚑', + '🚒', + '🚓', + '🚔', + '🚕', + '🚖', + '🚗', + '🚘', + '🚚', + '🚛', + '🚜', + '🏎', + '🏍', + '🛵', + '🚲', + '🛴', + '🚏', + '🛤', + '⛽', + '🚨', + '🚥', + '🚦', + '🚧', + '⚓', + '⛵', + '🚤', + '🛳', + '⛴', + '🛥', + '🚢', + '✈', + '🛩', + '🛫', + '🛬', + '💺', + '🚁', + '🚟', + '🚠', + '🚡', + '🛰', + '🚀', + '🛸', + '🌠', + '🌌', + '⛱', + '🎆', + '🎇', + '🎑', + '💴', + '💵', + '💶', + '💷', + '🗿', + '🛂', + '🛃', + '🛄', + '🛅' +]); + +/// Map of all possible emojis along with their names in [Category.ACTIVITIES] +final Map activities = Map.fromIterables([ + 'Man in Suit Levitating', + 'Man Climbing', + 'Woman Climbing', + 'Horse Racing', + 'Skier', + 'Snowboarder', + 'Man Golfing', + 'Woman Golfing', + 'Man Surfing', + 'Woman Surfing', + 'Man Rowing Boat', + 'Woman Rowing Boat', + 'Man Swimming', + 'Woman Swimming', + 'Man Bouncing Ball', + 'Woman Bouncing Ball', + 'Man Lifting Weights', + 'Woman Lifting Weights', + 'Man Biking', + 'Woman Biking', + 'Man Mountain Biking', + 'Woman Mountain Biking', + 'Man Cartwheeling', + 'Woman Cartwheeling', + 'Men Wrestling', + 'Women Wrestling', + 'Man Playing Water Polo', + 'Woman Playing Water Polo', + 'Man Playing Handball', + 'Woman Playing Handball', + 'Man Juggling', + 'Woman Juggling', + 'Man in Lotus Position', + 'Woman in Lotus Position', + 'Circus Tent', + 'Skateboard', + 'Reminder Ribbon', + 'Admission Tickets', + 'Ticket', + 'Military Medal', + 'Trophy', + 'Sports Medal', + '1st Place Medal', + '2nd Place Medal', + '3rd Place Medal', + 'Soccer Ball', + 'Baseball', + 'Softball', + 'Basketball', + 'Volleyball', + 'American Football', + 'Rugby Football', + 'Tennis', + 'Flying Disc', + 'Bowling', + 'Cricket Game', + 'FieldPB Hockey', + 'Ice Hockey', + 'Lacrosse', + 'Ping Pong', + 'Badminton', + 'Boxing Glove', + 'Martial Arts Uniform', + 'Flag in Hole', + 'Ice Skate', + 'Fishing Pole', + 'Running Shirt', + 'Skis', + 'Sled', + 'Curling Stone', + 'Direct Hit', + 'Pool 8 Ball', + 'Video Game', + 'Slot Machine', + 'Game Die', + 'Jigsaw', + 'Chess Pawn', + 'Performing Arts', + 'Artist Palette', + 'Thread', + 'Yarn', + 'Musical Score', + 'Microphone', + 'Headphone', + 'Saxophone', + 'Guitar', + 'Musical Keyboard', + 'Trumpet', + 'Violin', + 'Drum', + 'Clapper Board', + 'Bow and Arrow' +], [ + '🕴', + '🧗', + '🧗', + '🏇', + '⛷', + '🏂', + '🏌️', + '🏌️', + '🏄', + '🏄', + '🚣', + '🚣', + '🏊', + '🏊', + '⛹️', + '⛹️', + '🏋️', + '🏋️', + '🚴', + '🚴', + '🚵', + '🚵', + '🤸', + '🤸', + '🤼', + '🤼', + '🤽', + '🤽', + '🤾', + '🤾', + '🤹', + '🤹', + '🧘🏻‍♂️', + '🧘🏻‍♀️', + '🎪', + '🛹', + '🎗', + '🎟', + '🎫', + '🎖', + '🏆', + '🏅', + '🥇', + '🥈', + '🥉', + '⚽', + '⚾', + '🥎', + '🏀', + '🏐', + '🏈', + '🏉', + '🎾', + '🥏', + '🎳', + '🏏', + '🏑', + '🏒', + '🥍', + '🏓', + '🏸', + '🥊', + '🥋', + '⛳', + '⛸', + '🎣', + '🎽', + '🎿', + '🛷', + '🥌', + '🎯', + '🎱', + '🎮', + '🎰', + '🎲', + '🧩', + '♟', + '🎭', + '🎨', + '🧵', + '🧶', + '🎼', + '🎤', + '🎧', + '🎷', + '🎸', + '🎹', + '🎺', + '🎻', + '🥁', + '🎬', + '🏹' +]); + +/// Map of all possible emojis along with their names in [Category.OBJECTS] +final Map objects = Map.fromIterables([ + 'Love Letter', + 'Hole', + 'Bomb', + 'Person Taking Bath', + 'Person in Bed', + 'Kitchen Knife', + 'Amphora', + 'World Map', + 'Compass', + 'Brick', + 'Barber Pole', + 'Oil Drum', + 'Bellhop Bell', + 'Luggage', + 'Hourglass Done', + 'Hourglass Not Done', + 'Watch', + 'Alarm Clock', + 'Stopwatch', + 'Timer Clock', + 'Mantelpiece Clock', + 'Thermometer', + 'Umbrella on Ground', + 'Firecracker', + 'Balloon', + 'Party Popper', + 'Confetti Ball', + 'Japanese Dolls', + 'Carp Streamer', + 'Wind Chime', + 'Red Envelope', + 'Ribbon', + 'Wrapped Gift', + 'Crystal Ball', + 'Nazar Amulet', + 'Joystick', + 'Teddy Bear', + 'Framed Picture', + 'Thread', + 'Yarn', + 'Shopping Bags', + 'Prayer Beads', + 'Gem Stone', + 'Postal Horn', + 'Studio Microphone', + 'Level Slider', + 'Control Knobs', + 'Radio', + 'Mobile Phone', + 'Mobile Phone With Arrow', + 'Telephone', + 'Telephone Receiver', + 'Pager', + 'Fax Machine', + 'Battery', + 'Electric Plug', + 'Laptop Computer', + 'Desktop Computer', + 'Printer', + 'Keyboard', + 'Computer Mouse', + 'Trackball', + 'Computer Disk', + 'Floppy Disk', + 'Optical Disk', + 'DVD', + 'Abacus', + 'Movie Camera', + 'Film Frames', + 'Film Projector', + 'Television', + 'Camera', + 'Camera With Flash', + 'Video Camera', + 'Videocassette', + 'Magnifying Glass Tilted Left', + 'Magnifying Glass Tilted Right', + 'Candle', + 'Light Bulb', + 'Flashlight', + 'Red Paper Lantern', + 'Notebook With Decorative Cover', + 'Closed Book', + 'Open Book', + 'Green Book', + 'Blue Book', + 'Orange Book', + 'Books', + 'Notebook', + 'Page With Curl', + 'Scroll', + 'Page Facing Up', + 'Newspaper', + 'Rolled-Up Newspaper', + 'Bookmark Tabs', + 'Bookmark', + 'Label', + 'Money Bag', + 'Yen Banknote', + 'Dollar Banknote', + 'Euro Banknote', + 'Pound Banknote', + 'Money With Wings', + 'Credit Card', + 'Receipt', + 'Envelope', + 'E-Mail', + 'Incoming Envelope', + 'Envelope With Arrow', + 'Outbox Tray', + 'Inbox Tray', + 'Package', + 'Closed Mailbox With Raised Flag', + 'Closed Mailbox With Lowered Flag', + 'Open Mailbox With Raised Flag', + 'Open Mailbox With Lowered Flag', + 'Postbox', + 'Ballot Box With Ballot', + 'Pencil', + 'Black Nib', + 'Fountain Pen', + 'Pen', + 'Paintbrush', + 'Crayon', + 'Memo', + 'File Folder', + 'Open File Folder', + 'Card Index Dividers', + 'Calendar', + 'Tear-Off Calendar', + 'Spiral Notepad', + 'Spiral Calendar', + 'Card Index', + 'Chart Increasing', + 'Chart Decreasing', + 'Bar Chart', + 'Clipboard', + 'Pushpin', + 'Round Pushpin', + 'Paperclip', + 'Linked Paperclips', + 'Straight Ruler', + 'Triangular Ruler', + 'Scissors', + 'Card File Box', + 'File Cabinet', + 'Wastebasket', + 'Locked', + 'Unlocked', + 'Locked With Pen', + 'Locked With Key', + 'Key', + 'Old Key', + 'Hammer', + 'Pick', + 'Hammer and Pick', + 'Hammer and Wrench', + 'Dagger', + 'Crossed Swords', + 'Pistol', + 'Shield', + 'Wrench', + 'Nut and Bolt', + 'Gear', + 'Clamp', + 'Balance Scale', + 'Link', + 'Chains', + 'Toolbox', + 'Magnet', + 'Alembic', + 'Test Tube', + 'Petri Dish', + 'DNA', + 'Microscope', + 'Telescope', + 'Satellite Antenna', + 'Syringe', + 'Pill', + 'Door', + 'Bed', + 'Couch and Lamp', + 'Toilet', + 'Shower', + 'Bathtub', + 'Lotion Bottle', + 'Safety Pin', + 'Broom', + 'Basket', + 'Roll of Paper', + 'Soap', + 'Sponge', + 'Fire Extinguisher', + 'Cigarette', + 'Coffin', + 'Funeral Urn', + 'Moai', + 'Potable Water' +], [ + '💌', + '🕳', + '💣', + '🛀', + '🛌', + '🔪', + '🏺', + '🗺', + '🧭', + '🧱', + '💈', + '🛢', + '🛎', + '🧳', + '⌛', + '⏳', + '⌚', + '⏰', + '⏱', + '⏲', + '🕰', + '🌡', + '⛱', + '🧨', + '🎈', + '🎉', + '🎊', + '🎎', + '🎏', + '🎐', + '🧧', + '🎀', + '🎁', + '🔮', + '🧿', + '🕹', + '🧸', + '🖼', + '🧵', + '🧶', + '🛍', + '📿', + '💎', + '📯', + '🎙', + '🎚', + '🎛', + '📻', + '📱', + '📲', + '☎', + '📞', + '📟', + '📠', + '🔋', + '🔌', + '💻', + '🖥', + '🖨', + '⌨', + '🖱', + '🖲', + '💽', + '💾', + '💿', + '📀', + '🧮', + '🎥', + '🎞', + '📽', + '📺', + '📷', + '📸', + '📹', + '📼', + '🔍', + '🔎', + '🕯', + '💡', + '🔦', + '🏮', + '📔', + '📕', + '📖', + '📗', + '📘', + '📙', + '📚', + '📓', + '📃', + '📜', + '📄', + '📰', + '🗞', + '📑', + '🔖', + '🏷', + '💰', + '💴', + '💵', + '💶', + '💷', + '💸', + '💳', + '🧾', + '✉', + '📧', + '📨', + '📩', + '📤', + '📥', + '📦', + '📫', + '📪', + '📬', + '📭', + '📮', + '🗳', + '✏', + '✒', + '🖋', + '🖊', + '🖌', + '🖍', + '📝', + '📁', + '📂', + '🗂', + '📅', + '📆', + '🗒', + '🗓', + '📇', + '📈', + '📉', + '📊', + '📋', + '📌', + '📍', + '📎', + '🖇', + '📏', + '📐', + '✂', + '🗃', + '🗄', + '🗑', + '🔒', + '🔓', + '🔏', + '🔐', + '🔑', + '🗝', + '🔨', + '⛏', + '⚒', + '🛠', + '🗡', + '⚔', + '🔫', + '🛡', + '🔧', + '🔩', + '⚙', + '🗜', + '⚖', + '🔗', + '⛓', + '🧰', + '🧲', + '⚗', + '🧪', + '🧫', + '🧬', + '🔬', + '🔭', + '📡', + '💉', + '💊', + '🚪', + '🛏', + '🛋', + '🚽', + '🚿', + '🛁', + '🧴', + '🧷', + '🧹', + '🧺', + '🧻', + '🧼', + '🧽', + '🧯', + '🚬', + '⚰', + '⚱', + '🗿', + '🚰' +]); + +/// Map of all possible emojis along with their names in [Category.SYMBOLS] +final Map symbols = Map.fromIterables([ + 'Heart With Arrow', + 'Heart With Ribbon', + 'Sparkling Heart', + 'Growing Heart', + 'Beating Heart', + 'Revolving Hearts', + 'Two Hearts', + 'Heart Decoration', + 'Heavy Heart Exclamation', + 'Broken Heart', + 'Red Heart', + 'Orange Heart', + 'Yellow Heart', + 'Green Heart', + 'Blue Heart', + 'Purple Heart', + 'Black Heart', + 'Hundred Points', + 'Anger Symbol', + 'Speech Balloon', + 'Eye in Speech Bubble', + 'Right Anger Bubble', + 'Thought Balloon', + 'Zzz', + 'White Flower', + 'Hot Springs', + 'Barber Pole', + 'Stop Sign', + 'Twelve O’Clock', + 'Twelve-Thirty', + 'One O’Clock', + 'One-Thirty', + 'Two O’Clock', + 'Two-Thirty', + 'Three O’Clock', + 'Three-Thirty', + 'Four O’Clock', + 'Four-Thirty', + 'Five O’Clock', + 'Five-Thirty', + 'Six O’Clock', + 'Six-Thirty', + 'Seven O’Clock', + 'Seven-Thirty', + 'Eight O’Clock', + 'Eight-Thirty', + 'Nine O’Clock', + 'Nine-Thirty', + 'Ten O’Clock', + 'Ten-Thirty', + 'Eleven O’Clock', + 'Eleven-Thirty', + 'Cyclone', + 'Spade Suit', + 'Heart Suit', + 'Diamond Suit', + 'Club Suit', + 'Joker', + 'Mahjong Red Dragon', + 'Flower Playing Cards', + 'Muted Speaker', + 'Speaker Low Volume', + 'Speaker Medium Volume', + 'Speaker High Volume', + 'Loudspeaker', + 'Megaphone', + 'Postal Horn', + 'Bell', + 'Bell With Slash', + 'Musical Note', + 'Musical Notes', + 'ATM Sign', + 'Litter in Bin Sign', + 'Potable Water', + 'Wheelchair Symbol', + 'Men’s Room', + 'Women’s Room', + 'Restroom', + 'Baby Symbol', + 'Water Closet', + 'Warning', + 'Children Crossing', + 'No Entry', + 'Prohibited', + 'No Bicycles', + 'No Smoking', + 'No Littering', + 'Non-Potable Water', + 'No Pedestrians', + 'No One Under Eighteen', + 'Radioactive', + 'Biohazard', + 'Up Arrow', + 'Up-Right Arrow', + 'Right Arrow', + 'Down-Right Arrow', + 'Down Arrow', + 'Down-Left Arrow', + 'Left Arrow', + 'Up-Left Arrow', + 'Up-Down Arrow', + 'Left-Right Arrow', + 'Right Arrow Curving Left', + 'Left Arrow Curving Right', + 'Right Arrow Curving Up', + 'Right Arrow Curving Down', + 'Clockwise Vertical Arrows', + 'Counterclockwise Arrows Button', + 'Back Arrow', + 'End Arrow', + 'On! Arrow', + 'Soon Arrow', + 'Top Arrow', + 'Place of Worship', + 'Atom Symbol', + 'Om', + 'Star of David', + 'Wheel of Dharma', + 'Yin Yang', + 'Latin Cross', + 'Orthodox Cross', + 'Star and Crescent', + 'Peace Symbol', + 'Menorah', + 'Dotted Six-Pointed Star', + 'Aries', + 'Taurus', + 'Gemini', + 'Cancer', + 'Leo', + 'Virgo', + 'Libra', + 'Scorpio', + 'Sagittarius', + 'Capricorn', + 'Aquarius', + 'Pisces', + 'Ophiuchus', + 'Shuffle Tracks Button', + 'Repeat Button', + 'Repeat Single Button', + 'Play Button', + 'Fast-Forward Button', + 'Reverse Button', + 'Fast Reverse Button', + 'Upwards Button', + 'Fast Up Button', + 'Downwards Button', + 'Fast Down Button', + 'Stop Button', + 'Eject Button', + 'Cinema', + 'Dim Button', + 'Bright Button', + 'Antenna Bars', + 'Vibration Mode', + 'Mobile Phone Off', + 'Infinity', + 'Recycling Symbol', + 'Trident Emblem', + 'Name Badge', + 'Japanese Symbol for Beginner', + 'Heavy Large Circle', + 'White Heavy Check Mark', + 'Ballot Box With Check', + 'Heavy Check Mark', + 'Heavy Multiplication X', + 'Cross Mark', + 'Cross Mark Button', + 'Heavy Plus Sign', + 'Heavy Minus Sign', + 'Heavy Division Sign', + 'Curly Loop', + 'Double Curly Loop', + 'Part Alternation Mark', + 'Eight-Spoked Asterisk', + 'Eight-Pointed Star', + 'Sparkle', + 'Double Exclamation Mark', + 'Exclamation Question Mark', + 'Question Mark', + 'White Question Mark', + 'White Exclamation Mark', + 'Exclamation Mark', + 'Copyright', + 'Registered', + 'Trade Mark', + 'Keycap Number Sign', + 'Keycap Digit Zero', + 'Keycap Digit One', + 'Keycap Digit Two', + 'Keycap Digit Three', + 'Keycap Digit Four', + 'Keycap Digit Five', + 'Keycap Digit Six', + 'Keycap Digit Seven', + 'Keycap Digit Eight', + 'Keycap Digit Nine', + 'Keycap: 10', + 'Input Latin Uppercase', + 'Input Latin Lowercase', + 'Input Numbers', + 'Input Symbols', + 'Input Latin Letters', + 'A Button (Blood Type)', + 'AB Button (Blood Type)', + 'B Button (Blood Type)', + 'CL Button', + 'Cool Button', + 'Free Button', + 'Information', + 'ID Button', + 'Circled M', + 'New Button', + 'NG Button', + 'O Button (Blood Type)', + 'OK Button', + 'P Button', + 'SOS Button', + 'Up! Button', + 'Vs Button', + 'Japanese “Here” Button', + 'Japanese “Service Charge” Button', + 'Japanese “Monthly Amount” Button', + 'Japanese “Not Free of Charge” Button', + 'Japanese “Reserved” Button', + 'Japanese “Bargain” Button', + 'Japanese “Discount” Button', + 'Japanese “Free of Charge” Button', + 'Japanese “Prohibited” Button', + 'Japanese “Acceptable” Button', + 'Japanese “Application” Button', + 'Japanese “Passing Grade” Button', + 'Japanese “Vacancy” Button', + 'Japanese “Congratulations” Button', + 'Japanese “Secret” Button', + 'Japanese “Open for Business” Button', + 'Japanese “No Vacancy” Button', + 'Red Circle', + 'Blue Circle', + 'Black Circle', + 'White Circle', + 'Black Large Square', + 'White Large Square', + 'Black Medium Square', + 'White Medium Square', + 'Black Medium-Small Square', + 'White Medium-Small Square', + 'Black Small Square', + 'White Small Square', + 'Large Orange Diamond', + 'Large Blue Diamond', + 'Small Orange Diamond', + 'Small Blue Diamond', + 'Red Triangle Pointed Up', + 'Red Triangle Pointed Down', + 'Diamond With a Dot', + 'White Square Button', + 'Black Square Button' +], [ + '💘', + '💝', + '💖', + '💗', + '💓', + '💞', + '💕', + '💟', + '❣', + '💔', + '❤', + '🧡', + '💛', + '💚', + '💙', + '💜', + '🖤', + '💯', + '💢', + '💬', + '👁️‍🗨️', + '🗯', + '💭', + '💤', + '💮', + '♨', + '💈', + '🛑', + '🕛', + '🕧', + '🕐', + '🕜', + '🕑', + '🕝', + '🕒', + '🕞', + '🕓', + '🕟', + '🕔', + '🕠', + '🕕', + '🕡', + '🕖', + '🕢', + '🕗', + '🕣', + '🕘', + '🕤', + '🕙', + '🕥', + '🕚', + '🕦', + '🌀', + '♠', + '♥', + '♦', + '♣', + '🃏', + '🀄', + '🎴', + '🔇', + '🔈', + '🔉', + '🔊', + '📢', + '📣', + '📯', + '🔔', + '🔕', + '🎵', + '🎶', + '🏧', + '🚮', + '🚰', + '♿', + '🚹', + '🚺', + '🚻', + '🚼', + '🚾', + '⚠', + '🚸', + '⛔', + '🚫', + '🚳', + '🚭', + '🚯', + '🚱', + '🚷', + '🔞', + '☢', + '☣', + '⬆', + '↗', + '➡', + '↘', + '⬇', + '↙', + '⬅', + '↖', + '↕', + '↔', + '↩', + '↪', + '⤴', + '⤵', + '🔃', + '🔄', + '🔙', + '🔚', + '🔛', + '🔜', + '🔝', + '🛐', + '⚛', + '🕉', + '✡', + '☸', + '☯', + '✝', + '☦', + '☪', + '☮', + '🕎', + '🔯', + '♈', + '♉', + '♊', + '♋', + '♌', + '♍', + '♎', + '♏', + '♐', + '♑', + '♒', + '♓', + '⛎', + '🔀', + '🔁', + '🔂', + '▶', + '⏩', + '◀', + '⏪', + '🔼', + '⏫', + '🔽', + '⏬', + '⏹', + '⏏', + '🎦', + '🔅', + '🔆', + '📶', + '📳', + '📴', + '♾', + '♻', + '🔱', + '📛', + '🔰', + '⭕', + '✅', + '☑', + '✔', + '✖', + '❌', + '❎', + '➕', + '➖', + '➗', + '➰', + '➿', + '〽', + '✳', + '✴', + '❇', + '‼', + '⁉', + '❓', + '❔', + '❕', + '❗', + '©', + '®', + '™', + '#️⃣', + '0️⃣', + '1️⃣', + '2️⃣', + '3️⃣', + '4️⃣', + '5️⃣', + '6️⃣', + '7️⃣', + '8️⃣', + '9️⃣', + '🔟', + '🔠', + '🔡', + '🔢', + '🔣', + '🔤', + '🅰', + '🆎', + '🅱', + '🆑', + '🆒', + '🆓', + 'ℹ', + '🆔', + 'Ⓜ', + '🆕', + '🆖', + '🅾', + '🆗', + '🅿', + '🆘', + '🆙', + '🆚', + '🈁', + '🈂', + '🈷', + '🈶', + '🈯', + '🉐', + '🈹', + '🈚', + '🈲', + '🉑', + '🈸', + '🈴', + '🈳', + '㊗', + '㊙', + '🈺', + '🈵', + '🔴', + '🔵', + '⚫', + '⚪', + '⬛', + '⬜', + '◼', + '◻', + '◾', + '◽', + '▪', + '▫', + '🔶', + '🔷', + '🔸', + '🔹', + '🔺', + '🔻', + '💠', + '🔳', + '🔲' +]); + +/// Map of all possible emojis along with their names in [Category.FLAGS] +final Map flags = Map.fromIterables([ + 'Chequered Flag', + 'Triangular Flag', + 'Crossed Flags', + 'Black Flag', + 'White Flag', + 'Rainbow Flag', + 'Pirate Flag', + 'Flag: Ascension Island', + 'Flag: Andorra', + 'Flag: United Arab Emirates', + 'Flag: Afghanistan', + 'Flag: Antigua & Barbuda', + 'Flag: Anguilla', + 'Flag: Albania', + 'Flag: Armenia', + 'Flag: Angola', + 'Flag: Antarctica', + 'Flag: argentina', + 'Flag: American Samoa', + 'Flag: Austria', + 'Flag: Australia', + 'Flag: Aruba', + 'Flag: Åland Islands', + 'Flag: Azerbaijan', + 'Flag: Bosnia & Herzegovina', + 'Flag: Barbados', + 'Flag: Bangladesh', + 'Flag: Belgium', + 'Flag: Burkina Faso', + 'Flag: Bulgaria', + 'Flag: Bahrain', + 'Flag: Burundi', + 'Flag: Benin', + 'Flag: St. Barthélemy', + 'Flag: Bermuda', + 'Flag: Brunei', + 'Flag: Bolivia', + 'Flag: Caribbean Netherlands', + 'Flag: Brazil', + 'Flag: Bahamas', + 'Flag: Bhutan', + 'Flag: Bouvet Island', + 'Flag: Botswana', + 'Flag: Belarus', + 'Flag: Belize', + 'Flag: Canada', + 'Flag: Cocos (Keeling) Islands', + 'Flag: Congo - Kinshasa', + 'Flag: Central African Republic', + 'Flag: Congo - Brazzaville', + 'Flag: Switzerland', + 'Flag: Côte d’Ivoire', + 'Flag: Cook Islands', + 'Flag: Chile', + 'Flag: Cameroon', + 'Flag: China', + 'Flag: Colombia', + 'Flag: Clipperton Island', + 'Flag: Costa Rica', + 'Flag: Cuba', + 'Flag: Cape Verde', + 'Flag: Curaçao', + 'Flag: Christmas Island', + 'Flag: Cyprus', + 'Flag: Czechia', + 'Flag: Germany', + 'Flag: Diego Garcia', + 'Flag: Djibouti', + 'Flag: Denmark', + 'Flag: Dominica', + 'Flag: Dominican Republic', + 'Flag: Algeria', + 'Flag: Ceuta & Melilla', + 'Flag: Ecuador', + 'Flag: Estonia', + 'Flag: Egypt', + 'Flag: Western Sahara', + 'Flag: Eritrea', + 'Flag: Spain', + 'Flag: Ethiopia', + 'Flag: European Union', + 'Flag: Finland', + 'Flag: Fiji', + 'Flag: Falkland Islands', + 'Flag: Micronesia', + 'Flag: Faroe Islands', + 'Flag: france', + 'Flag: Gabon', + 'Flag: United Kingdom', + 'Flag: Grenada', + 'Flag: Georgia', + 'Flag: French Guiana', + 'Flag: Guernsey', + 'Flag: Ghana', + 'Flag: Gibraltar', + 'Flag: Greenland', + 'Flag: Gambia', + 'Flag: Guinea', + 'Flag: Guadeloupe', + 'Flag: Equatorial Guinea', + 'Flag: Greece', + 'Flag: South Georgia & South Sandwich Islands', + 'Flag: Guatemala', + 'Flag: Guam', + 'Flag: Guinea-Bissau', + 'Flag: Guyana', + 'Flag: Hong Kong SAR China', + 'Flag: Heard & McDonald Islands', + 'Flag: Honduras', + 'Flag: Croatia', + 'Flag: Haiti', + 'Flag: Hungary', + 'Flag: Canary Islands', + 'Flag: Indonesia', + 'Flag: Ireland', + 'Flag: Israel', + 'Flag: Isle of Man', + 'Flag: India', + 'Flag: British Indian Ocean Territory', + 'Flag: Iraq', + 'Flag: Iran', + 'Flag: Iceland', + 'Flag: Italy', + 'Flag: Jersey', + 'Flag: Jamaica', + 'Flag: Jordan', + 'Flag: Japan', + 'Flag: Kenya', + 'Flag: Kyrgyzstan', + 'Flag: Cambodia', + 'Flag: Kiribati', + 'Flag: Comoros', + 'Flag: St. Kitts & Nevis', + 'Flag: North Korea', + 'Flag: South Korea', + 'Flag: Kuwait', + 'Flag: Cayman Islands', + 'Flag: Kazakhstan', + 'Flag: Laos', + 'Flag: Lebanon', + 'Flag: St. Lucia', + 'Flag: Liechtenstein', + 'Flag: Sri Lanka', + 'Flag: Liberia', + 'Flag: Lesotho', + 'Flag: Lithuania', + 'Flag: Luxembourg', + 'Flag: Latvia', + 'Flag: Libya', + 'Flag: Morocco', + 'Flag: Monaco', + 'Flag: Moldova', + 'Flag: Montenegro', + 'Flag: St. Martin', + 'Flag: Madagascar', + 'Flag: Marshall Islands', + 'Flag: North Macedonia', + 'Flag: Mali', + 'Flag: Myanmar (Burma)', + 'Flag: Mongolia', + 'Flag: Macau Sar China', + 'Flag: Northern Mariana Islands', + 'Flag: Martinique', + 'Flag: Mauritania', + 'Flag: Montserrat', + 'Flag: Malta', + 'Flag: Mauritius', + 'Flag: Maldives', + 'Flag: Malawi', + 'Flag: Mexico', + 'Flag: Malaysia', + 'Flag: Mozambique', + 'Flag: Namibia', + 'Flag: New Caledonia', + 'Flag: Niger', + 'Flag: Norfolk Island', + 'Flag: Nigeria', + 'Flag: Nicaragua', + 'Flag: Netherlands', + 'Flag: Norway', + 'Flag: Nepal', + 'Flag: Nauru', + 'Flag: Niue', + 'Flag: New Zealand', + 'Flag: Oman', + 'Flag: Panama', + 'Flag: Peru', + 'Flag: French Polynesia', + 'Flag: Papua New Guinea', + 'Flag: Philippines', + 'Flag: Pakistan', + 'Flag: Poland', + 'Flag: St. Pierre & Miquelon', + 'Flag: Pitcairn Islands', + 'Flag: Puerto Rico', + 'Flag: Palestinian Territories', + 'Flag: Portugal', + 'Flag: Palau', + 'Flag: Paraguay', + 'Flag: Qatar', + 'Flag: Réunion', + 'Flag: Romania', + 'Flag: Serbia', + 'Flag: Russia', + 'Flag: Rwanda', + 'Flag: Saudi Arabia', + 'Flag: Solomon Islands', + 'Flag: Seychelles', + 'Flag: Sudan', + 'Flag: Sweden', + 'Flag: Singapore', + 'Flag: St. Helena', + 'Flag: Slovenia', + 'Flag: Svalbard & Jan Mayen', + 'Flag: Slovakia', + 'Flag: Sierra Leone', + 'Flag: San Marino', + 'Flag: Senegal', + 'Flag: Somalia', + 'Flag: Suriname', + 'Flag: South Sudan', + 'Flag: São Tomé & Príncipe', + 'Flag: El Salvador', + 'Flag: Sint Maarten', + 'Flag: Syria', + 'Flag: Swaziland', + 'Flag: Tristan Da Cunha', + 'Flag: Turks & Caicos Islands', + 'Flag: Chad', + 'Flag: French Southern Territories', + 'Flag: Togo', + 'Flag: Thailand', + 'Flag: Tajikistan', + 'Flag: Tokelau', + 'Flag: Timor-Leste', + 'Flag: Turkmenistan', + 'Flag: Tunisia', + 'Flag: Tonga', + 'Flag: Turkey', + 'Flag: Trinidad & Tobago', + 'Flag: Tuvalu', + 'Flag: Taiwan', + 'Flag: Tanzania', + 'Flag: Ukraine', + 'Flag: Uganda', + 'Flag: U.S. Outlying Islands', + 'Flag: United Nations', + 'Flag: United States', + 'Flag: Uruguay', + 'Flag: Uzbekistan', + 'Flag: Vatican City', + 'Flag: St. Vincent & Grenadines', + 'Flag: Venezuela', + 'Flag: British Virgin Islands', + 'Flag: U.S. Virgin Islands', + 'Flag: Vietnam', + 'Flag: Vanuatu', + 'Flag: Wallis & Futuna', + 'Flag: Samoa', + 'Flag: Kosovo', + 'Flag: Yemen', + 'Flag: Mayotte', + 'Flag: South Africa', + 'Flag: Zambia', + 'Flag: Zimbabwe' +], [ + '🏁', + '🚩', + '🎌', + '🏴', + '🏳', + '🏳️‍🌈', + '🏴‍☠️', + '🇦🇨', + '🇦🇩', + '🇦🇪', + '🇦🇫', + '🇦🇬', + '🇦🇮', + '🇦🇱', + '🇦🇲', + '🇦🇴', + '🇦🇶', + '🇦🇷', + '🇦🇸', + '🇦🇹', + '🇦🇺', + '🇦🇼', + '🇦🇽', + '🇦🇿', + '🇧🇦', + '🇧🇧', + '🇧🇩', + '🇧🇪', + '🇧🇫', + '🇧🇬', + '🇧🇭', + '🇧🇮', + '🇧🇯', + '🇧🇱', + '🇧🇲', + '🇧🇳', + '🇧🇴', + '🇧🇶', + '🇧🇷', + '🇧🇸', + '🇧🇹', + '🇧🇻', + '🇧🇼', + '🇧🇾', + '🇧🇿', + '🇨🇦', + '🇨🇨', + '🇨🇩', + '🇨🇫', + '🇨🇬', + '🇨🇭', + '🇨🇮', + '🇨🇰', + '🇨🇱', + '🇨🇲', + '🇨🇳', + '🇨🇴', + '🇨🇵', + '🇨🇷', + '🇨🇺', + '🇨🇻', + '🇨🇼', + '🇨🇽', + '🇨🇾', + '🇨🇿', + '🇩🇪', + '🇩🇬', + '🇩🇯', + '🇩🇰', + '🇩🇲', + '🇩🇴', + '🇩🇿', + '🇪🇦', + '🇪🇨', + '🇪🇪', + '🇪🇬', + '🇪🇭', + '🇪🇷', + '🇪🇸', + '🇪🇹', + '🇪🇺', + '🇫🇮', + '🇫🇯', + '🇫🇰', + '🇫🇲', + '🇫🇴', + '🇫🇷', + '🇬🇦', + '🇬🇧', + '🇬🇩', + '🇬🇪', + '🇬🇫', + '🇬🇬', + '🇬🇭', + '🇬🇮', + '🇬🇱', + '🇬🇲', + '🇬🇳', + '🇬🇵', + '🇬🇶', + '🇬🇷', + '🇬🇸', + '🇬🇹', + '🇬🇺', + '🇬🇼', + '🇬🇾', + '🇭🇰', + '🇭🇲', + '🇭🇳', + '🇭🇷', + '🇭🇹', + '🇭🇺', + '🇮🇨', + '🇮🇩', + '🇮🇪', + '🇮🇱', + '🇮🇲', + '🇮🇳', + '🇮🇴', + '🇮🇶', + '🇮🇷', + '🇮🇸', + '🇮🇹', + '🇯🇪', + '🇯🇲', + '🇯🇴', + '🇯🇵', + '🇰🇪', + '🇰🇬', + '🇰🇭', + '🇰🇮', + '🇰🇲', + '🇰🇳', + '🇰🇵', + '🇰🇷', + '🇰🇼', + '🇰🇾', + '🇰🇿', + '🇱🇦', + '🇱🇧', + '🇱🇨', + '🇱🇮', + '🇱🇰', + '🇱🇷', + '🇱🇸', + '🇱🇹', + '🇱🇺', + '🇱🇻', + '🇱🇾', + '🇲🇦', + '🇲🇨', + '🇲🇩', + '🇲🇪', + '🇲🇫', + '🇲🇬', + '🇲🇭', + '🇲🇰', + '🇲🇱', + '🇲🇲', + '🇲🇳', + '🇲🇴', + '🇲🇵', + '🇲🇶', + '🇲🇷', + '🇲🇸', + '🇲🇹', + '🇲🇺', + '🇲🇻', + '🇲🇼', + '🇲🇽', + '🇲🇾', + '🇲🇿', + '🇳🇦', + '🇳🇨', + '🇳🇪', + '🇳🇫', + '🇳🇬', + '🇳🇮', + '🇳🇱', + '🇳🇴', + '🇳🇵', + '🇳🇷', + '🇳🇺', + '🇳🇿', + '🇴🇲', + '🇵🇦', + '🇵🇪', + '🇵🇫', + '🇵🇬', + '🇵🇭', + '🇵🇰', + '🇵🇱', + '🇵🇲', + '🇵🇳', + '🇵🇷', + '🇵🇸', + '🇵🇹', + '🇵🇼', + '🇵🇾', + '🇶🇦', + '🇷🇪', + '🇷🇴', + '🇷🇸', + '🇷🇺', + '🇷🇼', + '🇸🇦', + '🇸🇧', + '🇸🇨', + '🇸🇩', + '🇸🇪', + '🇸🇬', + '🇸🇭', + '🇸🇮', + '🇸🇯', + '🇸🇰', + '🇸🇱', + '🇸🇲', + '🇸🇳', + '🇸🇴', + '🇸🇷', + '🇸🇸', + '🇸🇹', + '🇸🇻', + '🇸🇽', + '🇸🇾', + '🇸🇿', + '🇹🇦', + '🇹🇨', + '🇹🇩', + '🇹🇫', + '🇹🇬', + '🇹🇭', + '🇹🇯', + '🇹🇰', + '🇹🇱', + '🇹🇲', + '🇹🇳', + '🇹🇴', + '🇹🇷', + '🇹🇹', + '🇹🇻', + '🇹🇼', + '🇹🇿', + '🇺🇦', + '🇺🇬', + '🇺🇲', + '🇺🇳', + '🇺🇸', + '🇺🇾', + '🇺🇿', + '🇻🇦', + '🇻🇨', + '🇻🇪', + '🇻🇬', + '🇻🇮', + '🇻🇳', + '🇻🇺', + '🇼🇫', + '🇼🇸', + '🇽🇰', + '🇾🇪', + '🇾🇹', + '🇿🇦', + '🇿🇲', + '🇿🇼' +]); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker.dart new file mode 100644 index 0000000000..ca45d4d155 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker.dart @@ -0,0 +1,312 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'models/category_models.dart'; +import 'config.dart'; +import 'default_emoji_picker_view.dart'; +import 'models/emoji_model.dart'; +import 'emoji_lists.dart' as emoji_list; +import 'emoji_view_state.dart'; +import 'models/recent_emoji_model.dart'; + +/// All the possible categories that [Emoji] can be put into +/// +/// All [Category] are shown in the category bar +enum Category { + /// Searched emojis + SEARCH, + + /// Recent emojis + RECENT, + + /// Smiley emojis + SMILEYS, + + /// Animal emojis + ANIMALS, + + /// Food emojis + FOODS, + + /// Activity emojis + ACTIVITIES, + + /// Travel emojis + TRAVEL, + + /// Objects emojis + OBJECTS, + + /// Sumbol emojis + SYMBOLS, + + /// Flag emojis + FLAGS, +} + +/// Enum to alter the keyboard button style +enum ButtonMode { + /// Android button style - gives the button a splash color with ripple effect + MATERIAL, + + /// iOS button style - gives the button a fade out effect when pressed + CUPERTINO +} + +/// Callback function for when emoji is selected +/// +/// The function returns the selected [Emoji] as well +/// as the [Category] from which it originated +typedef OnEmojiSelected = void Function(Category category, Emoji emoji); + +/// Callback function for backspace button +typedef OnBackspacePressed = void Function(); + +/// Callback function for custom view +typedef EmojiViewBuilder = Widget Function(Config config, EmojiViewState state); + +/// The Emoji Keyboard widget +/// +/// This widget displays a grid of [Emoji] sorted by [Category] +/// which the user can horizontally scroll through. +/// +/// There is also a bottombar which displays all the possible [Category] +/// and allow the user to quickly switch to that [Category] +class EmojiPicker extends StatefulWidget { + /// EmojiPicker for flutter + const EmojiPicker({ + Key? key, + required this.onEmojiSelected, + this.onBackspacePressed, + this.config = const Config(), + this.customWidget, + }) : super(key: key); + + /// Custom widget + final EmojiViewBuilder? customWidget; + + /// The function called when the emoji is selected + final OnEmojiSelected onEmojiSelected; + + /// The function called when backspace button is pressed + final OnBackspacePressed? onBackspacePressed; + + /// Config for customizations + final Config config; + + @override + EmojiPickerState createState() => EmojiPickerState(); +} + +class EmojiPickerState extends State { + static const platform = MethodChannel('emoji_picker_flutter'); + + List categoryEmoji = List.empty(growable: true); + List recentEmoji = List.empty(growable: true); + late Future updateEmojiFuture; + + // Prevent emojis to be reloaded with every build + bool loaded = false; + + @override + void initState() { + super.initState(); + updateEmojiFuture = _updateEmojis(); + } + + @override + void didUpdateWidget(covariant EmojiPicker oldWidget) { + if (oldWidget.config != widget.config) { + // Config changed - rebuild EmojiPickerView completely + loaded = false; + updateEmojiFuture = _updateEmojis(); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + if (!loaded) { + // Load emojis + updateEmojiFuture.then( + (value) => WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + loaded = true; + }); + }), + ); + + // Show loading indicator + return const Center(child: CircularProgressIndicator()); + } + if (widget.config.showRecentsTab) { + categoryEmoji[0].emoji = + recentEmoji.map((e) => e.emoji).toList().cast(); + } + + final state = EmojiViewState( + categoryEmoji, + _getOnEmojiListener(), + widget.onBackspacePressed, + ); + + // Build + return widget.customWidget == null + ? DefaultEmojiPickerView(widget.config, state) + : widget.customWidget!(widget.config, state); + } + + // Add recent emoji handling to tap listener + OnEmojiSelected _getOnEmojiListener() { + return (category, emoji) { + if (widget.config.showRecentsTab) { + _addEmojiToRecentlyUsed(emoji).then((value) { + if (category != Category.RECENT && mounted) { + setState(() { + // rebuild to update recent emoji tab + // when it is not current tab + }); + } + }); + } + widget.onEmojiSelected(category, emoji); + }; + } + + // Initialize emoji data + Future _updateEmojis() async { + categoryEmoji.clear(); + if (widget.config.showRecentsTab) { + recentEmoji = await _getRecentEmojis(); + final List recentEmojiMap = + recentEmoji.map((e) => e.emoji).toList().cast(); + categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap)); + } + categoryEmoji.addAll([ + CategoryEmoji(Category.SMILEYS, + await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),), + CategoryEmoji(Category.ANIMALS, + await _getAvailableEmojis(emoji_list.animals, title: 'animals'),), + CategoryEmoji(Category.FOODS, + await _getAvailableEmojis(emoji_list.foods, title: 'foods'),), + CategoryEmoji( + Category.ACTIVITIES, + await _getAvailableEmojis(emoji_list.activities, + title: 'activities',),), + CategoryEmoji(Category.TRAVEL, + await _getAvailableEmojis(emoji_list.travel, title: 'travel'),), + CategoryEmoji(Category.OBJECTS, + await _getAvailableEmojis(emoji_list.objects, title: 'objects'),), + CategoryEmoji(Category.SYMBOLS, + await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),), + CategoryEmoji(Category.FLAGS, + await _getAvailableEmojis(emoji_list.flags, title: 'flags'),) + ]); + } + + // Get available emoji for given category title + Future> _getAvailableEmojis(Map map, + {required String title,}) async { + Map? newMap; + + // Get Emojis cached locally if available + newMap = await _restoreFilteredEmojis(title); + + if (newMap == null) { + // Check if emoji is available on this platform + newMap = await _getPlatformAvailableEmoji(map); + // Save available Emojis to local storage for faster loading next time + if (newMap != null) { + await _cacheFilteredEmojis(title, newMap); + } + } + + // Map to Emoji Object + return newMap!.entries + .map((entry) => Emoji(entry.key, entry.value)) + .toList(); + } + + // Check if emoji is available on current platform + Future?> _getPlatformAvailableEmoji( + Map emoji,) async { + if (Platform.isAndroid) { + Map? filtered = {}; + const delimiter = '|'; + try { + final entries = emoji.values.join(delimiter); + final keys = emoji.keys.join(delimiter); + final result = (await platform.invokeMethod('checkAvailability', + {'emojiKeys': keys, 'emojiEntries': entries},)) as String; + final resultKeys = result.split(delimiter); + for (var i = 0; i < resultKeys.length; i++) { + filtered[resultKeys[i]] = emoji[resultKeys[i]]!; + } + } on PlatformException catch (_) { + filtered = null; + } + return filtered; + } else { + return emoji; + } + } + + // Restore locally cached emoji + Future?> _restoreFilteredEmojis(String title) async { + final prefs = await SharedPreferences.getInstance(); + final emojiJson = prefs.getString(title); + if (emojiJson == null) { + return null; + } + final emojis = + Map.from(jsonDecode(emojiJson) as Map); + return emojis; + } + + // Stores filtered emoji locally for faster access next time + Future _cacheFilteredEmojis( + String title, Map emojis,) async { + final prefs = await SharedPreferences.getInstance(); + final emojiJson = jsonEncode(emojis); + prefs.setString(title, emojiJson); + } + + // Returns list of recently used emoji from cache + Future> _getRecentEmojis() async { + final prefs = await SharedPreferences.getInstance(); + final emojiJson = prefs.getString('recent'); + if (emojiJson == null) { + return []; + } + final json = jsonDecode(emojiJson) as List; + return json.map(RecentEmoji.fromJson).toList(); + } + + // Add an emoji to recently used list or increase its counter + Future _addEmojiToRecentlyUsed(Emoji emoji) async { + final prefs = await SharedPreferences.getInstance(); + final recentEmojiIndex = + recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji); + if (recentEmojiIndex != -1) { + // Already exist in recent list + // Just update counter + recentEmoji[recentEmojiIndex].counter++; + } else { + recentEmoji.add(RecentEmoji(emoji, 1)); + } + // Sort by counter desc + recentEmoji.sort((a, b) => b.counter - a.counter); + // Limit entries to recentsLimit + recentEmoji = recentEmoji.sublist( + 0, min(widget.config.recentsLimit, recentEmoji.length),); + // save locally + prefs.setString('recent', jsonEncode(recentEmoji)); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker_builder.dart new file mode 100644 index 0000000000..fe526e01db --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_picker_builder.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import 'config.dart'; +import 'emoji_view_state.dart'; + +/// Template class for custom implementation +/// Inherit this class to create your own EmojiPicker +abstract class EmojiPickerBuilder extends StatefulWidget { + /// Constructor + const EmojiPickerBuilder(this.config, this.state, {Key? key}) + : super(key: key); + + /// Config for customizations + final Config config; + + /// State that holds current emoji data + final EmojiViewState state; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_view_state.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_view_state.dart new file mode 100644 index 0000000000..202f913715 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/emoji_view_state.dart @@ -0,0 +1,21 @@ +import 'models/category_models.dart'; +import 'emoji_picker.dart'; + +/// State that holds current emoji data +class EmojiViewState { + /// Constructor + EmojiViewState( + this.categoryEmoji, + this.onEmojiSelected, + this.onBackspacePressed, + ); + + /// List of all category including their emoji + final List categoryEmoji; + + /// Callback when pressed on emoji + final OnEmojiSelected onEmojiSelected; + + /// Callback when pressed on backspace + final OnBackspacePressed? onBackspacePressed; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/category_models.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/category_models.dart new file mode 100644 index 0000000000..885ff2ab0c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/category_models.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import 'emoji_model.dart'; +import '../emoji_picker.dart'; + +/// Container for Category and their emoji +class CategoryEmoji { + /// Constructor + CategoryEmoji(this.category, this.emoji); + + /// Category instance + final Category category; + + /// List of emoji of this category + List emoji; + + @override + String toString() { + return 'Name: $category, Emoji: $emoji'; + } +} + +/// Class that defines the icon representing a [Category] +class CategoryIcon { + /// Icon of Category + const CategoryIcon({ + required this.icon, + this.color = const Color(0xffd3d3d3), + this.selectedColor = const Color(0xffb2b2b2), + }); + + /// The icon to represent the category + final IconData icon; + + /// The default color of the icon + final Color color; + + /// The color of the icon once the category is selected + final Color selectedColor; +} + +/// Class used to define all the [CategoryIcon] shown for each [Category] +/// +/// This allows the keyboard to be personalized by changing icons shown. +/// If a [CategoryIcon] is set as null or not defined during initialization, +/// the default icons will be used instead +class CategoryIcons { + /// Constructor + const CategoryIcons({ + this.recentIcon = Icons.access_time, + this.smileyIcon = Icons.tag_faces, + this.animalIcon = Icons.pets, + this.foodIcon = Icons.fastfood, + this.activityIcon = Icons.directions_run, + this.travelIcon = Icons.location_city, + this.objectIcon = Icons.lightbulb_outline, + this.symbolIcon = Icons.emoji_symbols, + this.flagIcon = Icons.flag, + this.searchIcon = Icons.search, + }); + + /// Icon for [Category.RECENT] + final IconData recentIcon; + + /// Icon for [Category.SMILEYS] + final IconData smileyIcon; + + /// Icon for [Category.ANIMALS] + final IconData animalIcon; + + /// Icon for [Category.FOODS] + final IconData foodIcon; + + /// Icon for [Category.ACTIVITIES] + final IconData activityIcon; + + /// Icon for [Category.TRAVEL] + final IconData travelIcon; + + /// Icon for [Category.OBJECTS] + final IconData objectIcon; + + /// Icon for [Category.SYMBOLS] + final IconData symbolIcon; + + /// Icon for [Category.FLAGS] + final IconData flagIcon; + + /// Icon for [Category.SEARCH] + final IconData searchIcon; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/emoji_model.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/emoji_model.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/emoji_model.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/emoji_model.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/recent_emoji_model.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/recent_emoji_model.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/recent_emoji_model.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/src/models/recent_emoji_model.dart 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 deleted file mode 100644 index 6c09ca6a28..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ /dev/null @@ -1,164 +0,0 @@ -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_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({ - super.configuration, - }); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return ErrorBlockComponentWidget( - key: node.key, - node: node, - configuration: configuration, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (_) => true; -} - -class ErrorBlockComponentWidget extends BlockComponentStatefulWidget { - const ErrorBlockComponentWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => - _ErrorBlockComponentWidgetState(); -} - -class _ErrorBlockComponentWidgetState extends State - with BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - @override - Widget build(BuildContext context) { - Widget child = Container( - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - child: UniversalPlatform.isDesktopOrWeb - ? _buildDesktopErrorBlock(context) - : _buildMobileErrorBlock(context), - ); - - child = Padding( - padding: padding, - child: child, - ); - - 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: context.read(), - child: child, - ); - } - - return child; - } - - Widget _buildDesktopErrorBlock(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - children: [ - const HSpace(12), - FlowyText.regular( - LocaleKeys.document_errorBlock_parseError.tr(args: [node.type]), - ), - const Spacer(), - OutlinedRoundedButton( - text: LocaleKeys.document_errorBlock_copyBlockContent.tr(), - onTap: _copyBlockContent, - ), - 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, - ), - ), - ], - ), - ), - ); - } - - 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/extensions/flowy_tint_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart index 65ade628f1..522d9df1a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart @@ -1,3 +1,4 @@ + import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; 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 deleted file mode 100644 index 2d65c602f5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart +++ /dev/null @@ -1,371 +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: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 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(); - - 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, - ), - ), - ], - ), - ), - ), - ); - } -} - -class FindMenu extends StatefulWidget { - const FindMenu({ - super.key, - required this.editorState, - required this.searchService, - required this.showReplaceMenu, - required this.focusNode, - required this.onDismiss, - required this.onToggleShowReplace, - }); - - final EditorState editorState; - final SearchServiceV3 searchService; - - final bool showReplaceMenu; - final FocusNode focusNode; - - final VoidCallback onDismiss; - final void Function() onToggleShowReplace; - - @override - State createState() => _FindMenuState(); -} - -class _FindMenuState extends State { - final textController = TextEditingController(); - - bool caseSensitive = false; - - @override - void initState() { - super.initState(); - - widget.searchService.matchWrappers.addListener(_setState); - widget.searchService.currentSelectedIndex.addListener(_setState); - - textController.addListener(_searchPattern); - } - - @override - void dispose() { - widget.searchService.matchWrappers.removeListener(_setState); - widget.searchService.currentSelectedIndex.removeListener(_setState); - widget.searchService.dispose(); - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // the selectedIndex from searchService is 0-based - final selectedIndex = widget.searchService.selectedIndex + 1; - final matches = widget.searchService.matchWrappers.value; - return Row( - children: [ - const HSpace(4.0), - // expand/collapse button - _FindAndReplaceIcon( - icon: widget.showReplaceMenu - ? FlowySvgs.drop_menu_show_s - : FlowySvgs.drop_menu_hide_s, - tooltipText: '', - onPressed: widget.onToggleShowReplace, - ), - const HSpace(4.0), - // find text input - SizedBox( - width: 200, - 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(), - ); - }, - decoration: _buildInputDecoration( - LocaleKeys.findAndReplace_find.tr(), - ), - ), - ), - // the count of matches - Container( - constraints: const BoxConstraints(minWidth: 80), - padding: const EdgeInsets.symmetric(horizontal: 8.0), - alignment: Alignment.centerLeft, - child: FlowyText( - matches.isEmpty - ? LocaleKeys.findAndReplace_noResult.tr() - : '$selectedIndex of ${matches.length}', - ), - ), - const HSpace(4.0), - // case sensitive button - _FindAndReplaceIcon( - icon: FlowySvgs.text_s, - tooltipText: LocaleKeys.findAndReplace_caseSensitive.tr(), - onPressed: () => setState(() { - caseSensitive = !caseSensitive; - widget.searchService.caseSensitive = caseSensitive; - }), - isSelected: caseSensitive, - ), - const HSpace(4.0), - // previous match button - _FindAndReplaceIcon( - onPressed: () => widget.searchService.navigateToMatch(moveUp: true), - icon: FlowySvgs.arrow_up_s, - tooltipText: LocaleKeys.findAndReplace_previousMatch.tr(), - ), - const HSpace(4.0), - // next match button - _FindAndReplaceIcon( - onPressed: () => widget.searchService.navigateToMatch(), - icon: FlowySvgs.arrow_down_s, - tooltipText: LocaleKeys.findAndReplace_nextMatch.tr(), - ), - const HSpace(4.0), - _FindAndReplaceIcon( - onPressed: widget.onDismiss, - icon: FlowySvgs.close_s, - tooltipText: LocaleKeys.findAndReplace_close.tr(), - ), - const HSpace(4.0), - ], - ); - } - - void _searchPattern() { - widget.searchService.findAndHighlight(textController.text); - _setState(); - } - - void _setState() { - setState(() {}); - } -} - -class ReplaceMenu extends StatefulWidget { - const ReplaceMenu({ - super.key, - required this.editorState, - required this.searchService, - required this.focusNode, - }); - - final EditorState editorState; - final SearchServiceV3 searchService; - - final FocusNode focusNode; - - @override - State createState() => _ReplaceMenuState(); -} - -class _ReplaceMenuState extends State { - final textController = TextEditingController(); - - @override - void dispose() { - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - // placeholder for aligning the replace menu - const HSpace(30), - SizedBox( - width: 200, - 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(), - ); - }, - decoration: _buildInputDecoration( - LocaleKeys.findAndReplace_replace.tr(), - ), - ), - ), - _FindAndReplaceIcon( - onPressed: _replaceSelectedWord, - iconBuilder: (_) => const Icon( - Icons.find_replace_outlined, - size: 16, - ), - tooltipText: LocaleKeys.findAndReplace_replace.tr(), - ), - const HSpace(4.0), - _FindAndReplaceIcon( - iconBuilder: (_) => const Icon( - Icons.change_circle_outlined, - size: 16, - ), - tooltipText: LocaleKeys.findAndReplace_replaceAll.tr(), - onPressed: () => widget.searchService.replaceAllMatches( - textController.text, - ), - ), - ], - ); - } - - void _replaceSelectedWord() { - widget.searchService.replaceSelectedWord(textController.text); - } -} - -class _FindAndReplaceIcon extends StatelessWidget { - const _FindAndReplaceIcon({ - required this.onPressed, - required this.tooltipText, - this.icon, - this.iconBuilder, - this.isSelected, - }); - - final VoidCallback onPressed; - final FlowySvgData? icon; - final WidgetBuilder? iconBuilder; - final String tooltipText; - final bool? isSelected; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - width: 24, - height: 24, - onPressed: onPressed, - icon: iconBuilder?.call(context) ?? - (icon != null - ? FlowySvg(icon!, color: Theme.of(context).iconTheme.color) - : 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 deleted file mode 100644 index e0f63e57c7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_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/application/document_appearance_cubit.dart'; -import 'package:appflowy/shared/google_fonts_extension.dart'; -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_popover/appflowy_popover.dart'; -import 'package:collection/collection.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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class ThemeFontFamilySetting extends StatefulWidget { - const ThemeFontFamilySetting({ - super.key, - required this.currentFontFamily, - }); - - final String currentFontFamily; - static Key textFieldKey = const Key('FontFamilyTextField'); - static Key resetButtonKey = const Key('FontFamilyResetButton'); - static Key popoverKey = const Key('FontFamilyPopover'); - - @override - State createState() => _ThemeFontFamilySettingState(); -} - -class _ThemeFontFamilySettingState extends State { - @override - Widget build(BuildContext context) { - return SettingListTile( - label: LocaleKeys.settings_appearance_fontFamily_label.tr(), - resetButtonKey: ThemeFontFamilySetting.resetButtonKey, - onResetRequested: () { - context.read().resetFontFamily(); - context - .read() - .syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily); - }, - trailing: [ - FontFamilyDropDown(currentFontFamily: widget.currentFontFamily), - ], - ); - } -} - -class FontFamilyDropDown extends StatefulWidget { - const FontFamilyDropDown({ - super.key, - required this.currentFontFamily, - this.onOpen, - this.onClose, - this.onFontFamilyChanged, - this.child, - this.popoverController, - this.offset, - this.onResetFont, - }); - - final String currentFontFamily; - final VoidCallback? onOpen; - final VoidCallback? onClose; - final void Function(String fontFamily)? onFontFamilyChanged; - final Widget? child; - final PopoverController? popoverController; - final Offset? offset; - final VoidCallback? onResetFont; - - @override - State createState() => _FontFamilyDropDownState(); -} - -class _FontFamilyDropDownState extends State { - final List availableFonts = [ - defaultFontFamily, - ...GoogleFonts.asMap().keys, - ]; - final ValueNotifier query = ValueNotifier(''); - - @override - void dispose() { - query.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final currentValue = widget.currentFontFamily.fontFamilyDisplayName; - return SettingValueDropDown( - popoverKey: ThemeFontFamilySetting.popoverKey, - popoverController: widget.popoverController, - currentValue: currentValue, - margin: EdgeInsets.zero, - boxConstraints: const BoxConstraints( - maxWidth: 240, - maxHeight: 420, - ), - onClose: () { - query.value = ''; - widget.onClose?.call(); - }, - offset: widget.offset, - 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(() { - query.value = value; - }); - }, - ), - ), - Container(height: 1, color: Theme.of(context).dividerColor), - ValueListenableBuilder( - valueListenable: query, - builder: (context, value, child) { - var displayed = availableFonts; - if (value.isNotEmpty) { - displayed = availableFonts - .where( - (font) => font - .toLowerCase() - .contains(value.toLowerCase().toString()), - ) - .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]), - ), - ), - ), - ); - }, - ), - ], - ); - }, - ); - } - - Widget _fontFamilyItemButton( - BuildContext context, - TextStyle style, - ) { - final buttonFontFamily = - style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily; - return Tooltip( - message: buttonFontFamily, - waitDuration: const Duration(milliseconds: 150), - child: SizedBox( - key: ValueKey(buttonFontFamily), - height: 36, - child: FlowyButton( - onHover: (_) => FocusScope.of(context).unfocus(), - text: FlowyText( - buttonFontFamily.fontFamilyDisplayName, - fontFamily: buttonFontFamily, - figmaLineHeight: 20, - fontWeight: FontWeight.w400, - ), - rightIcon: - buttonFontFamily == widget.currentFontFamily.parseFontFamilyName() - ? const FlowySvg(FlowySvgs.toolbar_check_m) - : null, - onTap: () { - if (widget.onFontFamilyChanged != null) { - widget.onFontFamilyChanged!(buttonFontFamily); - } else { - if (widget.currentFontFamily.parseFontFamilyName() != - buttonFontFamily) { - context - .read() - .setFontFamily(buttonFontFamily); - context - .read() - .syncFontFamily(buttonFontFamily); - } - } - PopoverContainer.of(context).close(); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart deleted file mode 100644 index 60211b9024..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'dart:ui'; - -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -const String kLocalImagesKey = 'local_images'; - -List get builtInAssetImages => [ - 'assets/images/built_in_cover_images/m_cover_image_1.jpg', - 'assets/images/built_in_cover_images/m_cover_image_2.jpg', - 'assets/images/built_in_cover_images/m_cover_image_3.jpg', - 'assets/images/built_in_cover_images/m_cover_image_4.jpg', - 'assets/images/built_in_cover_images/m_cover_image_5.jpg', - 'assets/images/built_in_cover_images/m_cover_image_6.jpg', - ]; - -class ColorOption { - const ColorOption({ - required this.colorHex, - required this.name, - }); - - final String colorHex; - final String name; -} - -class CoverColorPicker extends StatefulWidget { - const CoverColorPicker({ - super.key, - this.selectedBackgroundColorHex, - required this.pickerBackgroundColor, - required this.backgroundColorOptions, - required this.pickerItemHoverColor, - required this.onSubmittedBackgroundColorHex, - }); - - final String? selectedBackgroundColorHex; - final Color pickerBackgroundColor; - final List backgroundColorOptions; - final Color pickerItemHoverColor; - final void Function(String color) onSubmittedBackgroundColorHex; - - @override - State createState() => _CoverColorPickerState(); -} - -class _CoverColorPickerState extends State { - final scrollController = ScrollController(); - - @override - void dispose() { - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - height: 30, - alignment: Alignment.center, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - platform: TargetPlatform.windows, - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: _buildColorItems( - widget.backgroundColorOptions, - widget.selectedBackgroundColorHex, - ), - ), - ), - ); - } - - Widget _buildColorItems(List options, String? selectedColor) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: options - .map( - (e) => ColorItem( - option: e, - isChecked: e.colorHex == selectedColor, - hoverColor: widget.pickerItemHoverColor, - onTap: widget.onSubmittedBackgroundColorHex, - ), - ) - .toList(), - ); - } -} - -@visibleForTesting -class ColorItem extends StatelessWidget { - const ColorItem({ - super.key, - required this.option, - required this.isChecked, - required this.hoverColor, - required this.onTap, - }); - - final ColorOption option; - final bool isChecked; - final Color hoverColor; - final void Function(String) onTap; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(right: 10.0), - child: InkWell( - customBorder: const CircleBorder(), - hoverColor: hoverColor, - onTap: () => onTap(option.colorHex), - child: SizedBox.square( - dimension: 25, - child: DecoratedBox( - decoration: BoxDecoration( - color: option.colorHex.tryToColor(), - shape: BoxShape.circle, - ), - child: isChecked - ? SizedBox.square( - child: Container( - margin: const EdgeInsets.all(1), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).cardColor, - width: 3.0, - ), - color: option.colorHex.tryToColor(), - shape: BoxShape.circle, - ), - ), - ) - : null, - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index b891aecb6e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart +++ /dev/null @@ -1,129 +0,0 @@ -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_editor/appflowy_editor.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -part 'cover_editor_bloc.freezed.dart'; - -class ChangeCoverPopoverBloc - extends Bloc { - ChangeCoverPopoverBloc({required this.editorState, required this.node}) - : super(const ChangeCoverPopoverState.initial()) { - SharedPreferences.getInstance().then((prefs) { - _prefs = prefs; - _initCompleter.complete(); - }); - - _dispatch(); - } - - final EditorState editorState; - final Node node; - final _initCompleter = Completer(); - late final SharedPreferences _prefs; - - void _dispatch() { - on((event, emit) async { - await event.map( - fetchPickedImagePaths: (fetchPickedImagePaths) async { - final imageNames = await _getPreviouslyPickedImagePaths(); - - emit( - ChangeCoverPopoverState.loaded( - imageNames, - selectLatestImage: fetchPickedImagePaths.selectLatestImage, - ), - ); - }, - deleteImage: (deleteImage) async { - final currentState = state; - final currentlySelectedImage = - node.attributes[DocumentHeaderBlockKeys.coverDetails]; - if (currentState is _Loaded) { - await _deleteImageInStorage(deleteImage.path); - if (currentlySelectedImage == deleteImage.path) { - _removeCoverImageFromNode(); - } - final updateImageList = currentState.imageNames - .where((path) => path != deleteImage.path) - .toList(); - _updateImagePathsInStorage(updateImageList); - emit(ChangeCoverPopoverState.loaded(updateImageList)); - } - }, - clearAllImages: (clearAllImages) async { - final currentState = state; - final currentlySelectedImage = - node.attributes[DocumentHeaderBlockKeys.coverDetails]; - - if (currentState is _Loaded) { - for (final image in currentState.imageNames) { - await _deleteImageInStorage(image); - if (currentlySelectedImage == image) { - _removeCoverImageFromNode(); - } - } - _updateImagePathsInStorage([]); - emit(const ChangeCoverPopoverState.loaded([])); - } - }, - ); - }); - } - - Future> _getPreviouslyPickedImagePaths() async { - await _initCompleter.future; - final imageNames = _prefs.getStringList(kLocalImagesKey) ?? []; - if (imageNames.isEmpty) { - return imageNames; - } - imageNames.removeWhere((name) => !File(name).existsSync()); - unawaited(_prefs.setStringList(kLocalImagesKey, imageNames)); - return imageNames; - } - - void _updateImagePathsInStorage(List imagePaths) async { - await _initCompleter.future; - await _prefs.setStringList(kLocalImagesKey, imagePaths); - } - - Future _deleteImageInStorage(String path) async { - final imageFile = File(path); - await imageFile.delete(); - } - - void _removeCoverImageFromNode() { - final transaction = editorState.transaction; - transaction.updateNode(node, { - DocumentHeaderBlockKeys.coverType: CoverType.none.toString(), - DocumentHeaderBlockKeys.icon: - node.attributes[DocumentHeaderBlockKeys.icon], - }); - editorState.apply(transaction); - } -} - -@freezed -class ChangeCoverPopoverEvent with _$ChangeCoverPopoverEvent { - const factory ChangeCoverPopoverEvent.fetchPickedImagePaths({ - @Default(false) bool selectLatestImage, - }) = _FetchPickedImagePaths; - - 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.loaded( - List imageNames, { - @Default(false) selectLatestImage, - }) = _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 deleted file mode 100644 index f5df4c0904..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart +++ /dev/null @@ -1,343 +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/header/custom_cover_picker_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/button.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/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class CoverImagePicker extends StatefulWidget { - const CoverImagePicker({ - super.key, - required this.onBackPressed, - required this.onFileSubmit, - }); - - final VoidCallback onBackPressed; - final Function(List paths) onFileSubmit; - - @override - State createState() => _CoverImagePickerState(); -} - -class _CoverImagePickerState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => CoverImagePickerBloc() - ..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: () {}, - ); - }, - 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), - ), - const VSpace(10), - NetworkImageUrlInput( - onAdd: (url) { - context - .read() - .add(CoverImagePickerEvent.urlSubmit(url)); - }, - ), - const VSpace(10), - ImagePickerActionButtons( - onBackPressed: () { - widget.onBackPressed(); - }, - onSave: () { - context - .read() - .add(CoverImagePickerEvent.saveToGallery(state)); - }, - ), - ], - ); - }, - ), - ), - ); - } -} - -class NetworkImageUrlInput extends StatefulWidget { - const NetworkImageUrlInput({super.key, required this.onAdd}); - - final void Function(String color) onAdd; - - @override - State createState() => _NetworkImageUrlInputState(); -} - -class _NetworkImageUrlInputState extends State { - TextEditingController urlController = TextEditingController(); - bool get buttonDisabled => urlController.text.isEmpty; - - @override - void initState() { - super.initState(); - urlController.addListener(_updateState); - } - - void _updateState() => setState(() {}); - - @override - void dispose() { - urlController.removeListener(_updateState); - urlController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - flex: 4, - child: FlowyTextField( - controller: urlController, - hintText: LocaleKeys.document_plugins_cover_enterImageUrl.tr(), - ), - ), - const SizedBox( - width: 5, - ), - Expanded( - child: RoundedTextButton( - onPressed: () { - urlController.text.isNotEmpty - ? widget.onAdd(urlController.text) - : null; - }, - hoverColor: Colors.transparent, - fillColor: buttonDisabled - ? Theme.of(context).disabledColor - : Theme.of(context).colorScheme.primary, - height: 36, - title: LocaleKeys.document_plugins_cover_add.tr(), - borderRadius: Corners.s8Border, - ), - ), - ], - ); - } -} - -class ImagePickerActionButtons extends StatelessWidget { - const ImagePickerActionButtons({ - super.key, - required this.onBackPressed, - required this.onSave, - }); - - final VoidCallback onBackPressed; - final VoidCallback onSave; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FlowyTextButton( - LocaleKeys.document_plugins_cover_back.tr(), - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.end, - onPressed: () => onBackPressed(), - ), - FlowyTextButton( - LocaleKeys.document_plugins_cover_saveToGallery.tr(), - onPressed: () => onSave(), - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.end, - fontColor: Theme.of(context).colorScheme.primary, - ), - ], - ); - } -} - -class CoverImagePreviewWidget extends StatefulWidget { - const CoverImagePreviewWidget({super.key, required this.state}); - - final CoverImagePickerState state; - - @override - State createState() => - _CoverImagePreviewWidgetState(); -} - -class _CoverImagePreviewWidgetState extends State { - DecoratedBox _buildFilePickerWidget(BuildContext ctx) { - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: Corners.s6Border, - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg( - FlowySvgs.add_s, - size: Size(20, 20), - ), - const SizedBox( - width: 3, - ), - FlowyText( - LocaleKeys.document_plugins_cover_pasteImageUrl.tr(), - ), - ], - ), - const VSpace(10), - FlowyText( - LocaleKeys.document_plugins_cover_or.tr(), - fontWeight: FontWeight.w300, - ), - const VSpace(10), - FlowyButton( - hoverColor: Theme.of(context).hoverColor, - onTap: () { - ctx - .read() - .add(const CoverImagePickerEvent.pickFileImage()); - }, - useIntrinsicWidth: true, - leftIcon: const FlowySvg( - FlowySvgs.document_s, - size: Size(20, 20), - ), - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.document_plugins_cover_pickFromFiles.tr(), - ), - ), - ], - ), - ); - } - - Positioned _buildImageDeleteButton(BuildContext ctx) { - return Positioned( - right: 10, - top: 10, - child: InkWell( - onTap: () { - ctx - .read() - .add(const CoverImagePickerEvent.deleteImage()); - }, - child: DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.onPrimary, - ), - child: const FlowySvg( - FlowySvgs.close_s, - size: Size(20, 20), - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Container( - height: 180, - alignment: Alignment.center, - 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, - ), - ), - ), - ), - widget.state.maybeWhen( - fileImage: (_) => _buildImageDeleteButton(context), - networkImage: (successOrFail) => successOrFail.fold( - (l) => _buildImageDeleteButton(context), - (r) => const SizedBox.shrink(), - ), - orElse: () => 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 deleted file mode 100644 index 64e21eb773..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart +++ /dev/null @@ -1,221 +0,0 @@ -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'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'cover_editor.dart'; - -part 'custom_cover_picker_bloc.freezed.dart'; - -class CoverImagePickerBloc - extends Bloc { - CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) { - _dispatch(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.map( - initialEvent: (initialEvent) { - emit(const CoverImagePickerState.initial()); - }, - urlSubmit: (urlSubmit) async { - emit(const CoverImagePickerState.loading()); - final validateImage = await _validateURL(urlSubmit.path); - if (validateImage) { - emit( - CoverImagePickerState.networkImage( - FlowyResult.success(urlSubmit.path), - ), - ); - } else { - emit( - CoverImagePickerState.networkImage( - FlowyResult.failure( - FlowyError( - msg: LocaleKeys.document_plugins_cover_couldNotFetchImage - .tr(), - ), - ), - ), - ); - } - }, - pickFileImage: (pickFileImage) async { - final imagePickerResults = await _pickImages(); - if (imagePickerResults != null) { - emit(CoverImagePickerState.fileImage(imagePickerResults)); - } else { - emit(const CoverImagePickerState.initial()); - } - }, - deleteImage: (deleteImage) { - emit(const CoverImagePickerState.initial()); - }, - saveToGallery: (saveToGallery) async { - emit(const CoverImagePickerState.loading()); - final saveImage = await _saveToGallery(saveToGallery.previousState); - if (saveImage != null) { - emit(CoverImagePickerState.done(FlowyResult.success(saveImage))); - } else { - emit( - CoverImagePickerState.done( - FlowyResult.failure( - FlowyError( - msg: LocaleKeys.document_plugins_cover_imageSavingFailed - .tr(), - ), - ), - ), - ); - emit(const CoverImagePickerState.initial()); - } - }, - ); - }, - ); - } - - Future?>? _saveToGallery(CoverImagePickerState state) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final List imagePaths = prefs.getStringList(kLocalImagesKey) ?? []; - final directory = await _coverPath(); - - if (state is _FileImagePicked) { - try { - final path = state.path; - final newPath = p.join(directory, p.split(path).last); - final newFile = await File(path).copy(newPath); - imagePaths.add(newFile.path); - } catch (e) { - return null; - } - } else if (state is _NetworkImagePicked) { - try { - final url = state.successOrFail.fold((path) => path, (r) => null); - if (url != null) { - final response = await http.get(Uri.parse(url)); - final newPath = p.join(directory, _networkImageName(url)); - final imageFile = File(newPath); - await imageFile.create(); - await imageFile.writeAsBytes(response.bodyBytes); - imagePaths.add(imageFile.absolute.path); - } else { - return null; - } - } catch (e) { - return null; - } - } - await prefs.setStringList(kLocalImagesKey, imagePaths); - return imagePaths; - } - - Future _pickImages() async { - final result = await getIt().pickFiles( - dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(), - type: FileType.image, - allowedExtensions: defaultImageExtensions, - ); - if (result != null && result.files.isNotEmpty) { - return result.files.first.path; - } - return null; - } - - Future _coverPath() async { - final directory = await getIt().getPath(); - return Directory(p.join(directory, 'covers')) - .create(recursive: true) - .then((value) => value.path); - } - - String _networkImageName(String url) { - return 'IMG_${DateTime.now().millisecondsSinceEpoch.toString()}.${_getExtension( - url, - fromNetwork: true, - )}'; - } - - String? _getExtension( - String path, { - bool fromNetwork = false, - }) { - String? ext; - if (!fromNetwork) { - final extension = p.extension(path); - if (extension.isEmpty) { - return null; - } - ext = extension; - } else { - final uri = Uri.parse(path); - final parameters = uri.queryParameters; - if (path.contains('unsplash')) { - final dl = parameters['dl']; - if (dl != null) { - ext = p.extension(dl); - } - } else { - ext = p.extension(path); - } - } - if (ext != null && ext.isNotEmpty) { - ext = ext.substring(1); - } - if (defaultImageExtensions.contains(ext)) { - return ext; - } - return null; - } - - Future _validateURL(String path) async { - final extension = _getExtension(path, fromNetwork: true); - if (extension == null) { - return false; - } - try { - final response = await http.head(Uri.parse(path)); - return response.statusCode == 200; - } catch (e) { - return false; - } - } -} - -@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.saveToGallery( - CoverImagePickerState previousState, - ) = _SaveToGallery; - const factory CoverImagePickerEvent.initialEvent() = _InitialEvent; -} - -@freezed -class CoverImagePickerState with _$CoverImagePickerState { - const factory CoverImagePickerState.initial() = _Initial; - const factory CoverImagePickerState.loading() = _Loading; - const factory CoverImagePickerState.networkImage( - FlowyResult successOrFail, - ) = _NetworkImagePicked; - const factory CoverImagePickerState.fileImage(String path) = _FileImagePicked; - - const factory CoverImagePickerState.done( - FlowyResult, FlowyError> successOrFail, - ) = _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 deleted file mode 100644 index 7265ef6f82..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart +++ /dev/null @@ -1,169 +0,0 @@ -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'; -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_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:string_validator/string_validator.dart'; - -/// This is a transitional component that can be removed once the desktop -/// supports immersive widgets, allowing for the exclusive use of the DocumentImmersiveCover component. -class DesktopCover extends StatefulWidget { - const DesktopCover({ - super.key, - required this.view, - required this.editorState, - required this.node, - required this.coverType, - this.coverDetails, - }); - - final ViewPB view; - final Node node; - final EditorState editorState; - final CoverType coverType; - final String? coverDetails; - - @override - State createState() => _DesktopCoverState(); -} - -class _DesktopCoverState extends State { - CoverType get coverType => CoverType.fromString( - widget.node.attributes[DocumentHeaderBlockKeys.coverType], - ); - String? get coverDetails => - widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; - - @override - Widget build(BuildContext context) { - if (widget.view.extra.isEmpty) { - return _buildCoverImageV1(); - } - - return _buildCoverImageV2(); - } - - // version > 0.5.5 - Widget _buildCoverImageV2() { - return BlocProvider( - create: (context) => DocumentImmersiveCoverBloc(view: widget.view) - ..add(const DocumentImmersiveCoverEvent.initial()), - child: - BlocBuilder( - builder: (context, state) { - final cover = state.cover; - final type = state.cover.type; - const height = kCoverHeight; - - if (type == PageStyleCoverImageType.customImage || - type == PageStyleCoverImageType.unsplashImage) { - final userProfilePB = - context.read().state.userProfilePB; - return SizedBox( - height: height, - width: double.infinity, - child: FlowyNetworkImage( - url: cover.value, - userProfilePB: userProfilePB, - ), - ); - } - - if (type == PageStyleCoverImageType.builtInImage) { - return SizedBox( - height: height, - width: double.infinity, - child: Image.asset( - PageStyleCoverImageType.builtInImagePath(cover.value), - fit: BoxFit.cover, - ), - ); - } - - if (type == PageStyleCoverImageType.pureColor) { - // try to parse the color from the tint id, - // if it fails, try to parse the color as a hex string - final color = FlowyTint.fromId(cover.value)?.color(context) ?? - cover.value.tryToColor(); - return Container( - height: height, - width: double.infinity, - color: color, - ); - } - - if (type == PageStyleCoverImageType.gradientColor) { - return Container( - height: height, - width: double.infinity, - decoration: BoxDecoration( - gradient: FlowyGradientColor.fromId(cover.value).linear, - ), - ); - } - - if (type == PageStyleCoverImageType.localImage) { - return SizedBox( - height: height, - width: double.infinity, - child: Image.file( - File(cover.value), - fit: BoxFit.cover, - ), - ); - } - - return const SizedBox.shrink(); - }, - ), - ); - } - - // version <= 0.5.5 - Widget _buildCoverImageV1() { - final detail = 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()) { - return const SizedBox.shrink(); - } - return Image.file( - imageFile, - fit: BoxFit.cover, - ); - case CoverType.asset: - return Image.asset( - PageStyleCoverImageType.builtInImagePath(detail), - fit: BoxFit.cover, - ); - case CoverType.color: - final color = widget.coverDetails?.tryToColor() ?? Colors.white; - return Container(color: color); - case CoverType.none: - return const SizedBox.shrink(); - } - } -} 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/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart deleted file mode 100644 index cda76233d6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ /dev/null @@ -1,219 +0,0 @@ -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({ - super.key, - required this.emoji, - this.emojiSize = 60, - }); - - final EmojiIconData emoji; - final double emojiSize; - - @override - State createState() => _EmojiIconWidgetState(); -} - -class _EmojiIconWidgetState extends State { - bool hover = true; - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => setHidden(false), - onExit: (_) => setHidden(true), - cursor: SystemMouseCursors.click, - child: Container( - decoration: BoxDecoration( - color: !hover - ? Theme.of(context) - .colorScheme - .inverseSurface - .withValues(alpha: 0.5) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - alignment: Alignment.center, - child: RawEmojiIconWidget( - emoji: widget.emoji, - emojiSize: widget.emojiSize, - ), - ), - ); - } - - void setHidden(bool value) { - if (hover == value) return; - setState(() { - hover = value; - }); - } -} - -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/i18n/editor_i18n.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart deleted file mode 100644 index 8029e2a870..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart +++ /dev/null @@ -1,673 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class EditorI18n extends AppFlowyEditorL10n { - // static AppFlowyEditorLocalizations current = EditorI18n(); - EditorI18n(); - - @override - String get bold { - return LocaleKeys.editor_bold.tr(); - } - - /// `Bulleted List` - @override - String get bulletedList { - return LocaleKeys.editor_bulletedList.tr(); - } - - /// `Checkbox` - @override - String get checkbox { - return LocaleKeys.editor_checkbox.tr(); - } - - /// `Embed Code` - @override - String get embedCode { - return LocaleKeys.editor_embedCode.tr(); - } - - /// `H1` - @override - String get heading1 { - return LocaleKeys.editor_heading1.tr(); - } - - /// `H2` - @override - String get heading2 { - return LocaleKeys.editor_heading2.tr(); - } - - /// `H3` - @override - String get heading3 { - return LocaleKeys.editor_heading3.tr(); - } - - /// `Highlight` - @override - String get highlight { - return LocaleKeys.editor_highlight.tr(); - } - - /// `Color` - @override - String get color { - return LocaleKeys.editor_color.tr(); - } - - /// `Image` - @override - String get image { - return LocaleKeys.editor_image.tr(); - } - - /// `Italic` - @override - String get italic { - return LocaleKeys.editor_italic.tr(); - } - - /// `Link` - @override - String get link { - return LocaleKeys.editor_link.tr(); - } - - /// `Numbered List` - @override - String get numberedList { - return LocaleKeys.editor_numberedList.tr(); - } - - /// `Quote` - @override - String get quote { - return LocaleKeys.editor_quote.tr(); - } - - /// `Strikethrough` - @override - String get strikethrough { - return LocaleKeys.editor_strikethrough.tr(); - } - - /// `Text` - @override - String get text { - return LocaleKeys.editor_text.tr(); - } - - /// `Underline` - @override - String get underline { - return LocaleKeys.editor_underline.tr(); - } - - /// `Default` - @override - String get fontColorDefault { - return LocaleKeys.editor_fontColorDefault.tr(); - } - - /// `Gray` - @override - String get fontColorGray { - return LocaleKeys.editor_fontColorGray.tr(); - } - - /// `Brown` - @override - String get fontColorBrown { - return LocaleKeys.editor_fontColorBrown.tr(); - } - - /// `Orange` - @override - String get fontColorOrange { - return LocaleKeys.editor_fontColorOrange.tr(); - } - - /// `Yellow` - @override - String get fontColorYellow { - return LocaleKeys.editor_fontColorYellow.tr(); - } - - /// `Green` - @override - String get fontColorGreen { - return LocaleKeys.editor_fontColorGreen.tr(); - } - - /// `Blue` - @override - String get fontColorBlue { - return LocaleKeys.editor_fontColorBlue.tr(); - } - - /// `Purple` - @override - String get fontColorPurple { - return LocaleKeys.editor_fontColorPurple.tr(); - } - - /// `Pink` - @override - String get fontColorPink { - return LocaleKeys.editor_fontColorPink.tr(); - } - - /// `Red` - @override - String get fontColorRed { - return LocaleKeys.editor_fontColorRed.tr(); - } - - /// `Default background` - @override - String get backgroundColorDefault { - return LocaleKeys.editor_backgroundColorDefault.tr(); - } - - /// `Gray background` - @override - String get backgroundColorGray { - return LocaleKeys.editor_backgroundColorGray.tr(); - } - - /// `Brown background` - @override - String get backgroundColorBrown { - return LocaleKeys.editor_backgroundColorBrown.tr(); - } - - /// `Orange background` - @override - String get backgroundColorOrange { - return LocaleKeys.editor_backgroundColorOrange.tr(); - } - - /// `Yellow background` - @override - String get backgroundColorYellow { - return LocaleKeys.editor_backgroundColorYellow.tr(); - } - - /// `Green background` - @override - String get backgroundColorGreen { - return LocaleKeys.editor_backgroundColorGreen.tr(); - } - - /// `Blue background` - @override - String get backgroundColorBlue { - return LocaleKeys.editor_backgroundColorBlue.tr(); - } - - /// `Purple background` - @override - String get backgroundColorPurple { - return LocaleKeys.editor_backgroundColorPurple.tr(); - } - - /// `Pink background` - @override - String get backgroundColorPink { - return LocaleKeys.editor_backgroundColorPink.tr(); - } - - /// `Red background` - @override - String get backgroundColorRed { - return LocaleKeys.editor_backgroundColorRed.tr(); - } - - /// `Done` - @override - String get done { - return LocaleKeys.editor_done.tr(); - } - - /// `Cancel` - @override - String get cancel { - return LocaleKeys.editor_cancel.tr(); - } - - /// `Tint 1` - @override - String get tint1 { - return LocaleKeys.editor_tint1.tr(); - } - - /// `Tint 2` - @override - String get tint2 { - return LocaleKeys.editor_tint2.tr(); - } - - /// `Tint 3` - @override - String get tint3 { - return LocaleKeys.editor_tint3.tr(); - } - - /// `Tint 4` - @override - String get tint4 { - return LocaleKeys.editor_tint4.tr(); - } - - /// `Tint 5` - @override - String get tint5 { - return LocaleKeys.editor_tint5.tr(); - } - - /// `Tint 6` - @override - String get tint6 { - return LocaleKeys.editor_tint6.tr(); - } - - /// `Tint 7` - @override - String get tint7 { - return LocaleKeys.editor_tint7.tr(); - } - - /// `Tint 8` - @override - String get tint8 { - return LocaleKeys.editor_tint8.tr(); - } - - /// `Tint 9` - @override - String get tint9 { - return LocaleKeys.editor_tint9.tr(); - } - - /// `Purple` - @override - String get lightLightTint1 { - return LocaleKeys.editor_lightLightTint1.tr(); - } - - /// `Pink` - @override - String get lightLightTint2 { - return LocaleKeys.editor_lightLightTint2.tr(); - } - - /// `Light Pink` - @override - String get lightLightTint3 { - return LocaleKeys.editor_lightLightTint3.tr(); - } - - /// `Orange` - @override - String get lightLightTint4 { - return LocaleKeys.editor_lightLightTint4.tr(); - } - - /// `Yellow` - @override - String get lightLightTint5 { - return LocaleKeys.editor_lightLightTint5.tr(); - } - - /// `Lime` - @override - String get lightLightTint6 { - return LocaleKeys.editor_lightLightTint6.tr(); - } - - /// `Green` - @override - String get lightLightTint7 { - return LocaleKeys.editor_lightLightTint7.tr(); - } - - /// `Aqua` - @override - String get lightLightTint8 { - return LocaleKeys.editor_lightLightTint8.tr(); - } - - /// `Blue` - @override - String get lightLightTint9 { - return LocaleKeys.editor_lightLightTint9.tr(); - } - - /// `URL` - @override - String get urlHint { - return LocaleKeys.editor_urlHint.tr(); - } - - /// `Heading 1` - @override - String get mobileHeading1 { - return LocaleKeys.editor_mobileHeading1.tr(); - } - - /// `Heading 2` - @override - String get mobileHeading2 { - return LocaleKeys.editor_mobileHeading2.tr(); - } - - /// `Heading 3` - @override - String get mobileHeading3 { - return LocaleKeys.editor_mobileHeading3.tr(); - } - - /// `Text Color` - @override - String get textColor { - return LocaleKeys.editor_textColor.tr(); - } - - /// `Background Color` - @override - String get backgroundColor { - return LocaleKeys.editor_backgroundColor.tr(); - } - - /// `Add your link` - @override - String get addYourLink { - return LocaleKeys.editor_addYourLink.tr(); - } - - /// `Open link` - @override - String get openLink { - return LocaleKeys.editor_openLink.tr(); - } - - /// `Copy link` - @override - String get copyLink { - return LocaleKeys.editor_copyLink.tr(); - } - - /// `Remove link` - @override - String get removeLink { - return LocaleKeys.editor_removeLink.tr(); - } - - /// `Edit link` - @override - String get editLink { - return LocaleKeys.editor_editLink.tr(); - } - - /// `Text` - @override - String get linkText { - return LocaleKeys.editor_linkText.tr(); - } - - /// `Please enter text` - @override - String get linkTextHint { - return LocaleKeys.editor_linkTextHint.tr(); - } - - /// `Please enter URL` - @override - String get linkAddressHint { - return LocaleKeys.editor_linkAddressHint.tr(); - } - - /// `Highlight color` - @override - String get highlightColor { - return LocaleKeys.editor_highlightColor.tr(); - } - - /// `Clear highlight color` - @override - String get clearHighlightColor { - return LocaleKeys.editor_clearHighlightColor.tr(); - } - - /// `Custom color` - @override - String get customColor { - return LocaleKeys.editor_customColor.tr(); - } - - /// `Hex value` - @override - String get hexValue { - return LocaleKeys.editor_hexValue.tr(); - } - - /// `Opacity` - @override - String get opacity { - return LocaleKeys.editor_opacity.tr(); - } - - /// `Reset to default color` - @override - String get resetToDefaultColor { - return LocaleKeys.editor_resetToDefaultColor.tr(); - } - - /// `LTR` - @override - String get ltr { - return LocaleKeys.editor_ltr.tr(); - } - - /// `RTL` - @override - String get rtl { - return LocaleKeys.editor_rtl.tr(); - } - - /// `Auto` - @override - String get auto { - return LocaleKeys.editor_auto.tr(); - } - - /// `Cut` - @override - String get cut { - return LocaleKeys.editor_cut.tr(); - } - - /// `Copy` - @override - String get copy { - return LocaleKeys.editor_copy.tr(); - } - - /// `Paste` - @override - String get paste { - return LocaleKeys.editor_paste.tr(); - } - - /// `Find` - @override - String get find { - return LocaleKeys.editor_find.tr(); - } - - /// `Previous match` - @override - String get previousMatch { - return LocaleKeys.editor_previousMatch.tr(); - } - - /// `Next match` - @override - String get nextMatch { - return LocaleKeys.editor_nextMatch.tr(); - } - - /// `Close` - @override - String get closeFind { - return LocaleKeys.editor_closeFind.tr(); - } - - /// `Replace` - @override - String get replace { - return LocaleKeys.editor_replace.tr(); - } - - /// `Replace all` - @override - String get replaceAll { - return LocaleKeys.editor_replaceAll.tr(); - } - - /// `Regex` - @override - String get regex { - return LocaleKeys.editor_regex.tr(); - } - - /// `Case sensitive` - @override - String get caseSensitive { - return LocaleKeys.editor_caseSensitive.tr(); - } - - /// `Upload Image` - @override - String get uploadImage { - return LocaleKeys.editor_uploadImage.tr(); - } - - /// `URL Image` - @override - String get urlImage { - return LocaleKeys.editor_urlImage.tr(); - } - - /// `Incorrect Link` - @override - String get incorrectLink { - return LocaleKeys.editor_incorrectLink.tr(); - } - - /// `Upload` - @override - String get upload { - return LocaleKeys.editor_upload.tr(); - } - - /// `Choose an image` - @override - String get chooseImage { - return LocaleKeys.editor_chooseImage.tr(); - } - - /// `Loading` - @override - String get loading { - return LocaleKeys.editor_loading.tr(); - } - - /// `Could not load the image` - @override - String get imageLoadFailed { - return LocaleKeys.editor_imageLoadFailed.tr(); - } - - /// `Divider` - @override - String get divider { - return LocaleKeys.editor_divider.tr(); - } - - /// `Table` - @override - String get table { - return LocaleKeys.editor_table.tr(); - } - - /// `Add before` - @override - String get colAddBefore { - return LocaleKeys.editor_colAddBefore.tr(); - } - - /// `Add before` - @override - String get rowAddBefore { - return LocaleKeys.editor_rowAddBefore.tr(); - } - - /// `Add after` - @override - String get colAddAfter { - return LocaleKeys.editor_colAddAfter.tr(); - } - - /// `Add after` - @override - String get rowAddAfter { - return LocaleKeys.editor_rowAddAfter.tr(); - } - - /// `Remove` - @override - String get colRemove { - return LocaleKeys.editor_colRemove.tr(); - } - - /// `Remove` - @override - String get rowRemove { - return LocaleKeys.editor_rowRemove.tr(); - } - - /// `Duplicate` - @override - String get colDuplicate { - return LocaleKeys.editor_colDuplicate.tr(); - } - - /// `Duplicate` - @override - String get rowDuplicate { - return LocaleKeys.editor_rowDuplicate.tr(); - } - - /// `Clear Content` - @override - String get colClear { - return LocaleKeys.editor_colClear.tr(); - } - - /// `Clear Content` - @override - String get rowClear { - return LocaleKeys.editor_rowClear.tr(); - } - - /// `Enter a / to insert a block, or start typing` - @override - String get slashPlaceHolder { - return LocaleKeys.editor_slashPlaceHolder.tr(); - } -} 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/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/custom_image_block_component/unsupport_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart deleted file mode 100644 index f0310a4aa5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart +++ /dev/null @@ -1,38 +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_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; - -class UnsupportedImageWidget extends StatelessWidget { - const UnsupportedImageWidget({super.key}); - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - child: FlowyHover( - style: HoverStyle(borderRadius: BorderRadius.circular(4)), - child: SizedBox( - height: 52, - child: Row( - children: [ - const HSpace(10), - const FlowySvg( - FlowySvgs.image_placeholder_s, - size: Size.square(24), - ), - const HSpace(10), - FlowyText(LocaleKeys.document_imageBlock_unableToLoadImage.tr()), - ], - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index b1c6c94213..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart +++ /dev/null @@ -1,40 +0,0 @@ -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: (_) {}, - ), - ), - ); - } -} 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 deleted file mode 100644 index df975de731..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ /dev/null @@ -1,395 +0,0 @@ -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/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/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:flowy_infra_ui/style_widget/snap_bar.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}); - - final Node node; - - @override - State createState() => ImagePlaceholderState(); -} - -class ImagePlaceholderState extends State { - final controller = PopoverController(); - final documentService = DocumentService(); - late final editorState = context.read(); - - 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( - borderRadius: BorderRadius.circular(4), - ), - child: SizedBox( - height: 52, - child: Row( - children: [ - const HSpace(10), - FlowySvg( - FlowySvgs.slash_menu_icon_image_s, - size: const Size.square(24), - color: Theme.of(context).hintColor, - ), - const HSpace(10), - ..._buildTrailing(context), - ], - ), - ), - ), - ); - - if (UniversalPlatform.isDesktopOrWeb) { - return AppFlowyPopover( - controller: controller, - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 540, - maxHeight: 360, - minHeight: 80, - ), - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (context) { - return UploadImageMenu( - allowMultipleImages: true, - limitMaximumImageSize: !_isLocalMode(), - supportTypes: const [ - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImages: (files) { - 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); - } - }); - }, - 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, - // 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, - ), - ); - } else { - return MobileBlockActionButtons( - node: widget.node, - editorState: editorState, - child: GestureDetector( - onTap: () { - editorState.updateSelectionWithReason(null, extraInfo: {}); - showUploadImageMenu(); - }, - child: child, - ), - ); - } - } - - List _buildTrailing(BuildContext context) { - if (errorMessage != null) { - return [ - Flexible( - child: FlowyText( - '${LocaleKeys.document_plugins_image_imageUploadFailed.tr()}: ${errorMessage!}', - maxLines: 3, - ), - ), - ]; - } else if (showLoading) { - return [ - FlowyText( - LocaleKeys.document_imageBlock_imageIsUploading.tr(), - ), - const HSpace(8), - const CircularProgressIndicator.adaptive(), - ]; - } 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, - ), - ), - ]; - } - } - - 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, - supportTypes: const [ - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImages: (files) async { - context.pop(); - - final items = files - .where((file) => file.path.isNotEmpty) - .map((file) => file.path) - .toList(); - - await insertMultipleLocalImages(items); - }, - onSelectedAIImage: (url) async { - context.pop(); - await insertAIImage(url); - }, - onSelectedNetworkImage: (url) async { - context.pop(); - await insertNetworkImage(url); - }, - ), - ); - }, - ); - } - } - - Future insertMultipleLocalImages(List urls) async { - controller.close(); - - if (urls.isEmpty) { - return; - } - - setState(() { - showLoading = true; - errorMessage = null; - }); - - 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(), - ); - } - } - - 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 insertMultipleLocalImages([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; - transaction.updateNode(widget.node, { - CustomImageBlockKeys.url: url, - CustomImageBlockKeys.imageType: CustomImageType.external.toIntValue(), - }); - await editorState.apply(transaction); - } - - bool _isLocalMode() { - return context.read().isLocalMode; - } -} 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 deleted file mode 100644 index 5a30a4dda5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart +++ /dev/null @@ -1,101 +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: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/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'; - -final customImageMenuItem = SelectionMenuItem( - getName: () => AppFlowyEditorL10n.current.image, - icon: (_, isSelected, style) => SelectionMenuIconWidget( - name: 'image', - isSelected: isSelected, - style: style, - ), - keywords: ['image', 'picture', 'img', 'photo'], - handler: (editorState, _, __) 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((_) { - 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) { - 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; - } - 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)); - - 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 deleted file mode 100644 index 74d1955312..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart +++ /dev/null @@ -1,132 +0,0 @@ -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/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 { - 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 copyToPath = p.join( - imagePath, - '${uuid()}${p.extension(localImagePath)}', - ); - await File(localImagePath).copy( - copyToPath, - ); - return copyToPath; - } catch (e) { - Log.error('cannot save image file', e); - return null; - } -} - -Future<(String? path, String? errorMessage)> saveImageToCloudStorage( - String localImagePath, - String documentId, -) async { - final documentService = DocumentService(); - Log.debug("Uploading image local path: $localImagePath"); - final result = await documentService.uploadFile( - localFilePath: localImagePath, - documentId: documentId, - ); - return result.fold( - (s) async { - await CustomImageCacheManager().putFile( - s.url, - File(localImagePath).readAsBytesSync(), - ); - 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); - } - }, - ); -} - -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 deleted file mode 100644 index cb7fd457e0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart +++ /dev/null @@ -1,18 +0,0 @@ -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'; - -final imageMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __, ___) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg), - actionHandler: (_, editorState) async { - final imagePlaceholderKey = GlobalKey(); - await editorState.insertEmptyImageBlock(imagePlaceholderKey); - - WidgetsBinding.instance.addPostFrameCallback((_) { - 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/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart deleted file mode 100644 index da37945bf5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ /dev/null @@ -1,304 +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/plugins/document/application/prelude.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.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, - required this.type, - required this.alignment, - required this.editable, - required this.onResize, - required this.width, - required this.src, - this.height, - this.onDoubleTap, - this.onStateChange, - }); - - final String src; - final CustomImageType type; - final double width; - final double? height; - final Alignment alignment; - final bool editable; - final VoidCallback? onDoubleTap; - final ValueChanged? onStateChange; - - final void Function(double width) onResize; - - @override - State createState() => _ResizableImageState(); -} - -const _kImageBlockComponentMinWidth = 30.0; - -class _ResizableImageState extends State { - final documentService = DocumentService(); - - double initialOffset = 0; - double moveDistance = 0; - Widget? _cacheImage; - - late double imageWidth; - - @visibleForTesting - bool onFocus = false; - - UserProfilePB? _userProfilePB; - - @override - void initState() { - super.initState(); - - imageWidth = widget.width; - - _userProfilePB = context.read()?.userProfile ?? - context.read().state.userProfilePB; - } - - @override - Widget build(BuildContext context) { - return Align( - alignment: widget.alignment, - child: SizedBox( - 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), - ), - ), - ), - ); - } - - Widget _buildResizableImage(BuildContext context) { - Widget child; - final src = widget.src; - if (isURL(src)) { - _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); - }); - }, - ); - }, - ); - - child = _cacheImage!; - } else { - // load local file - _cacheImage ??= Image.file(File(src)); - child = _cacheImage!; - } - return Stack( - children: [ - child, - if (widget.editable) ...[ - _buildEdgeGesture( - context, - top: 0, - left: 5, - bottom: 0, - width: 5, - onUpdate: (distance) => setState(() => moveDistance = distance), - ), - _buildEdgeGesture( - context, - top: 0, - right: 5, - bottom: 0, - width: 5, - onUpdate: (distance) => setState(() => moveDistance = -distance), - ), - ], - ], - ); - } - - Widget _buildLoading(BuildContext context) { - return SizedBox( - height: 150, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox.fromSize( - size: const Size(18, 18), - child: const CircularProgressIndicator(), - ), - SizedBox.fromSize(size: const Size(10, 10)), - Text(AppFlowyEditorL10n.current.loading), - ], - ), - ); - } - - Widget _buildEdgeGesture( - BuildContext context, { - double? top, - double? left, - double? right, - double? bottom, - double? width, - void Function(double distance)? onUpdate, - }) { - return Positioned( - top: top, - left: left, - right: right, - bottom: bottom, - width: width, - child: GestureDetector( - onHorizontalDragStart: (details) { - initialOffset = details.globalPosition.dx; - }, - onHorizontalDragUpdate: (details) { - if (onUpdate != null) { - double offset = details.globalPosition.dx - initialOffset; - if (widget.alignment == Alignment.center) { - offset *= 2.0; - } - onUpdate(offset); - } - }, - onHorizontalDragEnd: (details) { - imageWidth = - max(_kImageBlockComponentMinWidth, imageWidth - moveDistance); - initialOffset = 0; - moveDistance = 0; - - widget.onResize(imageWidth); - }, - child: MouseRegion( - cursor: SystemMouseCursors.resizeLeftRight, - child: onFocus - ? Center( - child: Container( - height: 40, - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), - borderRadius: const BorderRadius.all( - Radius.circular(5.0), - ), - border: Border.all(color: Colors.white), - ), - ), - ) - : null, - ), - ), - ); - } -} - -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, - width: width, - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 8.0), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - border: Border.all(color: Colors.grey.withValues(alpha: 0.6)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const FlowySvg( - FlowySvgs.broken_image_xl, - size: Size.square(36), - ), - FlowyText( - AppFlowyEditorL10n.current.imageLoadFailed, - fontSize: 14, - ), - const VSpace(4), - if (error != null) - FlowyText( - error, - textAlign: TextAlign.center, - color: Theme.of(context).hintColor.withValues(alpha: 0.6), - fontSize: 10, - maxLines: 2, - ), - const VSpace(12), - OutlinedRoundedButton( - text: LocaleKeys.chat_retry.tr(), - onTap: onRetry, - ), - ], - ), - ); - } - - String? _getErrorMessage() { - if (error is HttpExceptionWithStatus) { - return 'Error ${(error as HttpExceptionWithStatus).statusCode}'; - } - - return null; - } -} 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 deleted file mode 100644 index 949e946188..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:unsplash_client/unsplash_client.dart'; - -const _accessKeyA = 'YyD-LbW5bVolHWZBq5fWRM_'; -const _accessKeyB = '3ezkG2XchRFjhNTnK9TE'; -const _secretKeyA = '5z4EnxaXjWjWMnuBhc0Ku0u'; -const _secretKeyB = 'YW2bsYCZlO-REZaqmV6A'; - -enum UnsplashImageType { - // the creator name is under the image - halfScreen, - // the creator name is on the image - fullScreen, -} - -typedef OnSelectUnsplashImage = void Function(String url); - -class UnsplashImageWidget extends StatefulWidget { - const UnsplashImageWidget({ - super.key, - this.type = UnsplashImageType.halfScreen, - required this.onSelectUnsplashImage, - }); - - final UnsplashImageType type; - final OnSelectUnsplashImage onSelectUnsplashImage; - - @override - State createState() => _UnsplashImageWidgetState(); -} - -class _UnsplashImageWidgetState extends State { - final unsplash = UnsplashClient( - settings: const ClientSettings( - credentials: AppCredentials( - accessKey: _accessKeyA + _accessKeyB, - secretKey: _secretKeyA + _secretKeyB, - ), - ), - ); - - late Future> randomPhotos; - - String query = ''; - - @override - void initState() { - super.initState(); - randomPhotos = unsplash.photos - .random(count: 18, orientation: PhotoOrientation.landscape) - .goAndGet(); - } - - @override - void dispose() { - unsplash.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 44, - child: FlowyMobileSearchTextField( - onChanged: (keyword) => query = keyword, - onSubmitted: (_) => _search(), - ), - ), - const VSpace(12.0), - Expanded( - child: FutureBuilder( - future: randomPhotos, - builder: (context, value) { - final data = value.data; - if (!value.hasData || - value.connectionState != ConnectionState.done || - data == null || - data.isEmpty) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - return _UnsplashImages( - type: widget.type, - photos: data, - onSelectUnsplashImage: widget.onSelectUnsplashImage, - ); - }, - ), - ), - ], - ); - } - - void _search() { - setState(() { - randomPhotos = unsplash.photos - .random( - count: 18, - orientation: PhotoOrientation.landscape, - query: query, - ) - .goAndGet(); - }); - } -} - -class _UnsplashImages extends StatefulWidget { - const _UnsplashImages({ - required this.type, - required this.photos, - required this.onSelectUnsplashImage, - }); - - final UnsplashImageType type; - final List photos; - final OnSelectUnsplashImage onSelectUnsplashImage; - - @override - State<_UnsplashImages> createState() => _UnsplashImagesState(); -} - -class _UnsplashImagesState extends State<_UnsplashImages> { - int _selectedPhotoIndex = -1; - - @override - Widget build(BuildContext context) { - const mainAxisSpacing = 16.0; - final crossAxisCount = switch (widget.type) { - UnsplashImageType.halfScreen => 3, - UnsplashImageType.fullScreen => 2, - }; - final crossAxisSpacing = switch (widget.type) { - UnsplashImageType.halfScreen => 10.0, - UnsplashImageType.fullScreen => 16.0, - }; - - return GridView.count( - crossAxisCount: crossAxisCount, - mainAxisSpacing: mainAxisSpacing, - crossAxisSpacing: crossAxisSpacing, - childAspectRatio: 4 / 3, - children: widget.photos.asMap().entries.map((entry) { - final index = entry.key; - final photo = entry.value; - return _UnsplashImage( - type: widget.type, - photo: photo, - isSelected: index == _selectedPhotoIndex, - onTap: () { - widget.onSelectUnsplashImage(photo.urls.full.toString()); - setState(() => _selectedPhotoIndex = index); - }, - ); - }).toList(), - ); - } -} - -class _UnsplashImage extends StatelessWidget { - const _UnsplashImage({ - required this.type, - required this.photo, - required this.onTap, - required this.isSelected, - }); - - final UnsplashImageType type; - final Photo photo; - final VoidCallback onTap; - final bool isSelected; - - @override - Widget build(BuildContext context) { - final child = switch (type) { - UnsplashImageType.halfScreen => _buildHalfScreenImage(context), - UnsplashImageType.fullScreen => _buildFullScreenImage(context), - }; - - return GestureDetector( - onTap: onTap, - child: isSelected - ? Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide(width: 1.50, color: Color(0xFF00BCF0)), - borderRadius: BorderRadius.circular(8.0), - ), - ), - padding: const EdgeInsets.all(2.0), - child: child, - ) - : child, - ); - } - - Widget _buildHalfScreenImage(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Image.network( - photo.urls.thumb.toString(), - fit: BoxFit.cover, - ), - ), - const HSpace(2.0), - FlowyText('by ${photo.name}', fontSize: 10.0), - ], - ); - } - - Widget _buildFullScreenImage(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: Stack( - children: [ - LayoutBuilder( - builder: (_, constraints) => Image.network( - photo.urls.thumb.toString(), - fit: BoxFit.cover, - width: constraints.maxWidth, - height: constraints.maxHeight, - ), - ), - Positioned( - bottom: 9, - left: 10, - child: FlowyText.medium( - photo.name, - fontSize: 13.0, - color: Colors.white, - ), - ), - ], - ), - ); - } -} - -extension on Photo { - String get name { - if (user.username.isNotEmpty) { - return user.username; - } else if (user.name.isNotEmpty) { - return user.name; - } else if (user.email?.isNotEmpty == true) { - return user.email!; - } - - return user.id; - } -} 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/infra/svg.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart new file mode 100644 index 0000000000..0951b0bda3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class Svg extends StatelessWidget { + const Svg({ + Key? key, + this.name, + this.width, + this.height, + this.color, + this.number, + this.padding, + }) : super(key: key); + + final String? name; + final double? width; + final double? height; + final Color? color; + final int? number; + final EdgeInsets? padding; + + final _defaultWidth = 20.0; + final _defaultHeight = 20.0; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding ?? const EdgeInsets.all(0), + child: _buildSvg(), + ); + } + + Widget _buildSvg() { + if (name != null) { + return SvgPicture.asset( + 'assets/images/$name.svg', + colorFilter: + color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null, + fit: BoxFit.fill, + height: height, + width: width, + package: 'appflowy_editor_plugins', + ); + } else if (number != null) { + final numberText = + '$number.'; + return SvgPicture.string( + numberText, + width: width ?? _defaultWidth, + height: height ?? _defaultHeight, + ); + } + return Container(); + } +} 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 deleted file mode 100644 index e41bdc1114..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart +++ /dev/null @@ -1,184 +0,0 @@ -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_math_fork/flutter_math.dart'; -import 'package:provider/provider.dart'; - -class InlineMathEquationKeys { - const InlineMathEquationKeys._(); - - static const formula = 'formula'; -} - -class InlineMathEquation extends StatefulWidget { - const InlineMathEquation({ - super.key, - required this.formula, - required this.node, - required this.index, - this.textStyle, - }); - - final Node node; - final int index; - final String formula; - final TextStyle? textStyle; - - @override - State createState() => _InlineMathEquationState(); -} - -class _InlineMathEquationState extends State { - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return _IgnoreParentPointer( - child: AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (_) { - return MathInputTextField( - initialText: widget.formula, - onSubmit: (value) async { - popoverController.close(); - if (value == widget.formula) { - return; - } - final editorState = context.read(); - final transaction = editorState.transaction - ..formatText(widget.node, widget.index, 1, { - InlineMathEquationKeys.formula: value, - }); - await editorState.apply(transaction); - }, - ); - }, - offset: const Offset(0, 10), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: _buildMathEquation(context), - ), - ), - ), - ); - } - - 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 { - const MathInputTextField({ - super.key, - required this.initialText, - required this.onSubmit, - }); - - final String initialText; - final void Function(String value) onSubmit; - - @override - State createState() => _MathInputTextFieldState(); -} - -class _MathInputTextFieldState extends State { - late final TextEditingController textEditingController; - - @override - void initState() { - super.initState(); - - textEditingController = TextEditingController( - text: widget.initialText, - ); - textEditingController.selection = TextSelection( - baseOffset: 0, - extentOffset: widget.initialText.length, - ); - } - - @override - void dispose() { - textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 240, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: FlowyFormTextInput( - autoFocus: true, - textAlign: TextAlign.left, - controller: textEditingController, - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 4.0, - ), - onEditingComplete: () => - widget.onSubmit(textEditingController.text), - ), - ), - const HSpace(4.0), - FlowyButton( - text: FlowyText(LocaleKeys.button_done.tr()), - useIntrinsicWidth: true, - onTap: () => widget.onSubmit(textEditingController.text), - ), - ], - ), - ); - } -} - -class _IgnoreParentPointer extends StatelessWidget { - const _IgnoreParentPointer({ - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - onTapDown: (_) {}, - onDoubleTap: () {}, - onLongPress: () {}, - child: child, - ); - } -} 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 deleted file mode 100644 index cd3779cb6c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart +++ /dev/null @@ -1,87 +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/material.dart'; - -const _kInlineMathEquationToolbarItemId = 'editor.inline_math_equation'; - -final ToolbarItem inlineMathEquationItem = ToolbarItem( - id: _kInlineMathEquationToolbarItemId, - group: 4, - isActive: onlyShowInSingleSelectionAndTextType, - builder: (context, editorState, highlightColor, _, tooltipBuilder) { - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[InlineMathEquationKeys.formula] != null, - ); - }); - final child = SVGIconItemWidget( - iconBuilder: (_) => FlowySvg( - FlowySvgs.math_lg, - size: const Size.square(16), - color: isHighlight ? highlightColor : Colors.white, - ), - isHighlight: isHighlight, - highlightColor: highlightColor, - onPressed: () 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 transaction = editorState.transaction; - 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); - }, - ); - - 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 deleted file mode 100644 index 9be73fcc0b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ /dev/null @@ -1,217 +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/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({ - super.key, - required this.node, - required this.url, - this.title, - this.description, - this.imageUrl, - this.isHovering = false, - this.status = LinkLoadingStatus.loading, - }); - - final Node node; - final String? title; - 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 - .textStyleConfiguration - .text - .fontSize ?? - 16.0; - final isInDarkCallout = node.parent?.type == CalloutBlockKeys.type && - !Theme.of(context).isLightMode; - final (fontSize, width) = UniversalPlatform.isDesktopOrWeb - ? (documentFontSize, 160.0) - : (documentFontSize - 2, 120.0); - final Widget child = Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: Border.all( - color: isHovering || isInDarkCallout - ? borderScheme.greyTertiaryHover - : borderScheme.greyTertiary, - ), - borderRadius: BorderRadius.circular(16.0), - ), - child: SizedBox( - height: 96, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildImage(context), - 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, - ), - ], - ), - ), - ), - ], - ), - ), - ); - - if (UniversalPlatform.isDesktopOrWeb) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => afLaunchUrlString(url), - child: child, - ), - ); - } - - return MobileBlockActionButtons( - node: node, - editorState: context.read(), - extendActionWidgets: _buildExtendActionWidgets(context), - child: GestureDetector( - onTap: () => afLaunchUrlString(url), - child: child, - ), - ); - } - - // only used on mobile platform - List _buildExtendActionWidgets(BuildContext context) { - return [ - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_toolbar_link_m, - size: Size.square(18), - ), - onTap: () { - context.pop(); - convertUrlPreviewNodeToLink( - context.read(), - node, - ); - }, - ), - ]; - } - - 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_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart deleted file mode 100644 index 2fb493dda3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart +++ /dev/null @@ -1,207 +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_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/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: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 CustomLinkPreviewMenu extends StatefulWidget { - const CustomLinkPreviewMenu({ - super.key, - required this.onMenuShowed, - required this.onMenuHided, - required this.onReload, - required this.node, - }); - final VoidCallback onMenuShowed; - final VoidCallback onMenuHided; - final VoidCallback onReload; - final Node node; - - @override - State createState() => _CustomLinkPreviewMenuState(); -} - -class _CustomLinkPreviewMenuState extends State { - final popoverController = PopoverController(); - final buttonKey = GlobalKey(); - bool closed = false; - bool selected = false; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - widget.onMenuHided.call(); - } - - @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, - ), - ); - } - - 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 deleted file mode 100644 index 8b193c70fb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart +++ /dev/null @@ -1,202 +0,0 @@ -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, - }, - ); - 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 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); -} 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..b75eabb615 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,19 +1,13 @@ -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'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; 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._(); @@ -40,21 +34,17 @@ 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, - ), + name: 'MathEquation', + iconData: Icons.text_fields_rounded, keywords: ['tex, latex, katex', 'math equation', 'formula'], - nodeBuilder: (editorState, _) => mathEquationNode(), + 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 is _MathEquationBlockComponentWidgetState) { mathEquationState.showEditingDialog(); } }); @@ -64,9 +54,12 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node( class MathEquationBlockComponentBuilder extends BlockComponentBuilder { MathEquationBlockComponentBuilder({ - super.configuration, + this.configuration = const BlockComponentConfiguration(), }); + @override + final BlockComponentConfiguration configuration; + @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { final node = blockComponentContext.node; @@ -79,15 +72,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,16 +87,15 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @override State createState() => - MathEquationBlockComponentWidgetState(); + _MathEquationBlockComponentWidgetState(); } -class MathEquationBlockComponentWidgetState +class _MathEquationBlockComponentWidgetState extends State with BlockComponentConfigurable { @override @@ -116,145 +104,73 @@ 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), + child: _buildMathEquation(context), ); } - Widget _build(BuildContext context) { + Widget _buildMathEquation(BuildContext context) { Widget child = Container( - constraints: const BoxConstraints(minHeight: 52), + width: double.infinity, + constraints: const BoxConstraints(minHeight: 50), + padding: padding, decoration: BoxDecoration( - color: formula.isNotEmpty - ? Colors.transparent - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: isHover || formula.isEmpty + ? Theme.of(context).colorScheme.tertiaryContainer + : Colors.transparent, ), - child: FlowyHover( - style: HoverStyle( - borderRadius: BorderRadius.circular(4), - ), + child: Center( child: formula.isEmpty - ? _buildPlaceholderWidget(context) - : _buildMathEquation(context), + ? FlowyText.medium( + LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), + fontSize: 16, + ) + : Math.tex( + formula, + textStyle: const TextStyle(fontSize: 20), + mathStyle: MathStyle.display, + ), ), ); - if (widget.showActions && widget.actionBuilder != null) { + if (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(), - ), - ), - ], - ); - } - return child; } - Widget _buildPlaceholderWidget(BuildContext context) { - return SizedBox( - height: 52, - 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 HSpace(10), - FlowyText( - LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), - color: Theme.of(context).hintColor, - ), - ], - ), - ); - } - - Widget _buildMathEquation(BuildContext context) { - return Center( - child: Math.tex( - formula, - textStyle: const TextStyle(fontSize: 20), - ), - ); - } - - 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() { showDialog( context: context, builder: (context) { + final controller = TextEditingController(text: formula); return AlertDialog( backgroundColor: Theme.of(context).canvasColor, title: Text( LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(), ), - content: KeyboardListener( + content: RawKeyboardListener( focusNode: FocusNode(), - onKeyEvent: (key) { + onKey: (key) { + if (key is! RawKeyDownEvent) return; if (key.logicalKey == LogicalKeyboardKey.enter && - !HardwareKeyboard.instance.isShiftPressed) { + !key.isShiftPressed) { updateMathEquation(controller.text, context); } else if (key.logicalKey == LogicalKeyboardKey.escape) { dismiss(context); @@ -275,12 +191,11 @@ class MathEquationBlockComponentWidgetState ), actions: [ SecondaryTextButton( - LocaleKeys.button_cancel.tr(), - mode: TextButtonMode.big, + LocaleKeys.button_Cancel.tr(), onPressed: () => dismiss(context), ), PrimaryTextButton( - LocaleKeys.button_done.tr(), + LocaleKeys.button_Done.tr(), onPressed: () => updateMathEquation(controller.text, 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/math_equation/mobile_math_equation_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart deleted file mode 100644 index 62714ae93d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -final mathEquationMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __, ___) => const SizedBox( - width: 22, - child: FlowySvg(FlowySvgs.math_lg), - ), - actionHandler: (_, editorState) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final path = selection.start.path; - final node = editorState.getNodeAtPath(path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final transaction = editorState.transaction; - final insertedNode = mathEquationNode(); - - if (delta.isEmpty) { - transaction - ..insertNode(path, insertedNode) - ..deleteNode(node); - } else { - transaction.insertNode( - path.next, - insertedNode, - ); - } - - await editorState.apply(transaction); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final mathEquationState = - editorState.getNodeAtPath(path)?.key.currentState; - if (mathEquationState != null && - mathEquationState is MathEquationBlockComponentWidgetState) { - mathEquationState.showEditingDialog(); - } - }); - }, -); 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 deleted file mode 100644 index 0060d65bb7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ /dev/null @@ -1,178 +0,0 @@ -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; - - static MentionType fromString(String value) => switch (value) { - 'page' => page, - 'date' => date, - 'externalLink' => externalLink, - 'childPage' => childPage, - // Backwards compatibility - 'reminder' => date, - _ => throw UnimplementedError(), - }; -} - -Node dateMentionNode() { - return paragraphNode( - delta: Delta( - operations: [ - TextInsert( - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: DateTime.now().toIso8601String(), - reminderId: null, - reminderOption: null, - includeTime: false, - ), - ), - ], - ), - ); -} - -class MentionBlockKeys { - const MentionBlockKeys._(); - - 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 { - const MentionBlock({ - super.key, - required this.mention, - required this.node, - required this.index, - required this.textStyle, - }); - - final Map mention; - final Node node; - final int index; - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - final type = MentionType.fromString(mention[MentionBlockKeys.type]); - final editorState = context.read(); - - 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?; - - 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( - (o) => o.name == mention[MentionBlockKeys.reminderOption], - ); - - return MentionDateBlock( - key: ValueKey(date), - editorState: editorState, - date: date, - node: node, - textStyle: textStyle, - index: index, - reminderId: mention[MentionBlockKeys.reminderId], - reminderOption: reminderOption ?? ReminderOption.none, - 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, - ); - } - } -} 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 deleted file mode 100644 index 20f60be23d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ /dev/null @@ -1,378 +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/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/base/drag_handler.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/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/protobuf/flowy-user/reminder.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.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({ - super.key, - required this.editorState, - required this.date, - required this.index, - required this.node, - this.textStyle, - this.reminderId, - this.reminderOption = ReminderOption.none, - this.includeTime = false, - }); - - final EditorState editorState; - final String date; - final int index; - final Node node; - - /// If [isReminder] is true, then this must not be - /// null or empty - final String? reminderId; - - final ReminderOption reminderOption; - - final bool includeTime; - - final TextStyle? textStyle; - - @override - State createState() => _MentionDateBlockState(); -} - -class _MentionDateBlockState extends State { - 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(); - - if (appearance == null || reminder == null) { - return const SizedBox.shrink(); - } - - 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 options = DatePickerOptions( - focusedDay: parsedDate, - selectedDay: parsedDate, - includeTime: _includeTime, - dateFormat: appearance.dateFormat, - timeFormat: appearance.timeFormat, - selectedReminderOption: widget.reminderOption, - onIncludeTimeChanged: (includeTime, dateTime, _) { - _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 (widget.reminderOption != ReminderOption.none) { - _updateReminder( - widget.reminderOption, - reminder, - _includeTime, - ); - } else { - _updateBlock(selectedDay, includeTime: _includeTime); - } - }, - onReminderSelected: (reminderOption) => - _updateReminder(reminderOption, reminder), - ); - - 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, - ); - - // 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, - ), - const HSpace(4), - FlowySvg( - widget.reminderId != null - ? FlowySvgs.reminder_clock_s - : FlowySvgs.date_s, - size: Size.square(iconSize), - color: textStyle?.color, - ), - ], - ), - ), - ); - }, - ), - ); - } - - void _updateBlock( - DateTime date, { - required bool includeTime, - String? reminderId, - ReminderOption? reminderOption, - }) { - final rId = reminderId ?? - (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, - ), - ); - - widget.editorState.apply(transaction, withUpdateSelection: false); - - // Length of rendered block changes, this synchronizes - // the cursor with the new block render - widget.editorState.updateSelectionWithReason( - widget.editorState.selection, - ); - } - - void _updateReminder( - ReminderOption reminderOption, - ReminderPB? reminder, [ - bool includeTime = false, - ]) { - final rootContext = widget.editorState.document.root.context; - if (parsedDate == null || rootContext == null) { - return; - } - - if (widget.reminderId != null) { - _updateBlock( - parsedDate!, - includeTime: includeTime, - reminderOption: reminderOption, - ); - - if (ReminderOption.none == reminderOption && reminder != null) { - // Delete existing reminder - return rootContext - .read() - .add(ReminderEvent.remove(reminderId: reminder.id)); - } - - // Update existing reminder - return rootContext.read().add( - ReminderEvent.update( - ReminderUpdate( - id: widget.reminderId!, - scheduledAt: - reminderOption.getNotificationDateTime(parsedDate!), - date: parsedDate!, - ), - ), - ); - } - - final reminderId = nanoid(); - _updateBlock( - parsedDate!, - includeTime: includeTime, - reminderId: reminderId, - reminderOption: reminderOption, - ); - - // Add new reminder - final viewId = rootContext.read().documentId; - return rootContext.read().add( - ReminderEvent.add( - reminder: ReminderPB( - id: reminderId, - objectId: viewId, - title: LocaleKeys.reminderNotification_title.tr(), - message: LocaleKeys.reminderNotification_message.tr(), - 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()), - ), - ), - ); - } - - 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 deleted file mode 100644 index ede690eb30..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ /dev/null @@ -1,674 +0,0 @@ -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/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/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/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - show - ApplyOptions, - Delta, - EditorState, - Node, - NodeIterator, - Path, - Position, - Selection, - SelectionType, - 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'; - -final pageMemorizer = {}; - -Node pageMentionNode(String viewId) { - return paragraphNode( - delta: Delta( - operations: [ - TextInsert( - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: viewId, - blockId: null, - ), - ), - ], - ), - ); -} - -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, - }); - - final EditorState editorState; - final String pageId; - final String? blockId; - final Node node; - final TextStyle? textStyle; - - // Used to update the block - final int index; - - @override - State createState() => _MentionPageBlockState(); -} - -class _MentionPageBlockState extends State { - @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(); - } - - 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, - ), - ); - } - }, - ), - ); - } - - 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; - - @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; - } - - 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, - ), - ); - } else { - return _DesktopMentionPageBlock( - view: view, - showTrashHint: state.isInTrash, - content: null, - textStyle: widget.textStyle, - isChildPage: true, - handleTap: () => - handleMentionBlockTap(context, widget.editorState, view), - ); - } - }, - ), - ); - } - - Future fetchView(String pageId) async { - final view = await ViewBackendService.getView(pageId).then( - (value) => value.toNullable(), - ); - - if (view == null) { - // 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) { - return ViewPB() - ..id = trash.id - ..name = trash.name; - } - } - - return view; - } - - 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, - ), - ); - - 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 deleted file mode 100644 index f3578f185e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart +++ /dev/null @@ -1,148 +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/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/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( - BuildContext context, { - String? currentViewId, - String? selectedViewId, - bool Function(ViewPB view)? filter, -}) async { - filter ??= (v) => !v.isSpace && v.parentViewId.isNotEmpty; - - 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( - constraints: const BoxConstraints( - maxHeight: 340, - minHeight: 80, - ), - child: _MobilePageSelectorBody( - currentViewId: currentViewId, - selectedViewId: selectedViewId, - filter: filter, - ), - ), - ); -} - -class _MobilePageSelectorBody extends StatefulWidget { - const _MobilePageSelectorBody({ - this.currentViewId, - this.selectedViewId, - this.filter, - }); - - final String? currentViewId; - final String? selectedViewId; - final bool Function(ViewPB view)? filter; - - @override - State<_MobilePageSelectorBody> createState() => - _MobilePageSelectorBodyState(); -} - -class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> { - final searchController = TextEditingController(); - late final Future> _viewsFuture = _fetchViews(); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - height: 44.0, - child: FlowySearchTextField( - controller: searchController, - onChanged: (_) => setState(() {}), - ), - ), - FutureBuilder( - future: _viewsFuture, - builder: (_, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - if (snapshot.hasError || snapshot.data == null) { - return Center( - child: FlowyText( - LocaleKeys.document_mobilePageSelector_failedToLoad.tr(), - ), - ); - } - - final views = snapshot.data! - .where((v) => widget.filter?.call(v) ?? true) - .toList(); - - if (widget.currentViewId != null) { - views.removeWhere((v) => v.id == widget.currentViewId); - } - - final filtered = views.where( - (v) => - searchController.text.isEmpty || - v.name - .toLowerCase() - .contains(searchController.text.toLowerCase()), - ); - - if (filtered.isEmpty) { - return Center( - child: FlowyText( - LocaleKeys.document_mobilePageSelector_noPagesFound.tr(), - ), - ); - } - - 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), - ); - }, - ), - ); - }, - ), - ], - ); - } - - Future> _fetchViews() async => - (await ViewBackendService.getAllViews()).toNullable()?.items ?? []; -} 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..3918a2891e 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 @@ -1,17 +1,8 @@ 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_plugins/appflowy_editor_plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; -import 'package:string_validator/string_validator.dart'; class EditorMigration { // AppFlowy 0.1.x -> 0.2 @@ -54,9 +45,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 +55,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') { @@ -162,111 +141,15 @@ class EditorMigration { } const backgroundColor = 'backgroundColor'; if (attributes.containsKey(backgroundColor)) { - attributes[AppFlowyRichTextKeys.backgroundColor] = + attributes[FlowyRichTextKeys.highlightColor] = attributes[backgroundColor]; attributes.remove(backgroundColor); } const color = 'color'; if (attributes.containsKey(color)) { - attributes[AppFlowyRichTextKeys.textColor] = attributes[color]; + attributes[FlowyRichTextKeys.textColor] = attributes[color]; attributes.remove(color); } return attributes; } - - // Before version 0.5.5, the cover is stored in the document root. - // Now, the cover is stored in the view.ext. - static void migrateCoverIfNeeded( - ViewPB view, - Attributes attributes, { - bool overwrite = false, - }) async { - if (view.extra.isNotEmpty && !overwrite) { - return; - } - - final coverType = CoverType.fromString( - attributes[DocumentHeaderBlockKeys.coverType], - ); - final coverDetails = attributes[DocumentHeaderBlockKeys.coverDetails]; - - Map extra = {}; - - if (coverType == CoverType.none || - coverDetails == null || - coverDetails is! String) { - extra = { - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: PageStyleCoverImageType.none.toString(), - ViewExtKeys.coverValueKey: '', - }, - }; - } else { - switch (coverType) { - case CoverType.asset: - extra = { - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: - PageStyleCoverImageType.builtInImage.toString(), - ViewExtKeys.coverValueKey: coverDetails, - }, - }; - break; - case CoverType.color: - extra = { - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: - PageStyleCoverImageType.pureColor.toString(), - ViewExtKeys.coverValueKey: coverDetails, - }, - }; - break; - case CoverType.file: - if (isURL(coverDetails)) { - if (coverDetails.contains('unsplash')) { - extra = { - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: - PageStyleCoverImageType.unsplashImage.toString(), - ViewExtKeys.coverValueKey: coverDetails, - }, - }; - } else { - extra = { - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: - PageStyleCoverImageType.customImage.toString(), - ViewExtKeys.coverValueKey: coverDetails, - }, - }; - } - } else { - extra = { - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: - PageStyleCoverImageType.localImage.toString(), - ViewExtKeys.coverValueKey: coverDetails, - }, - }; - } - break; - default: - } - } - - if (extra.isEmpty) { - return; - } - - try { - final current = view.extra.isNotEmpty ? jsonDecode(view.extra) : {}; - final merged = mergeMaps(current, extra); - await ViewBackendService.updateView( - viewId: view.id, - extra: jsonEncode(merged), - ); - } catch (e) { - Log.error('Failed to migrating cover: $e'); - } - } } 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 deleted file mode 100644 index ba170e8d24..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart +++ /dev/null @@ -1,147 +0,0 @@ -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_animate/flutter_animate.dart'; - -List buildMobileFloatingToolbarItems( - EditorState editorState, - Offset offset, - Function closeToolbar, -) { - // copy, paste, select, select all, cut - final selection = editorState.selection; - if (selection == null) { - return []; - } - final toolbarItems = []; - - if (!selection.isCollapsed) { - toolbarItems.add( - ContextMenuButtonItem( - label: LocaleKeys.editor_copy.tr(), - onPressed: () { - customCopyCommand.execute(editorState); - closeToolbar(); - }, - ), - ); - } - - toolbarItems.add( - ContextMenuButtonItem( - label: LocaleKeys.editor_paste.tr(), - onPressed: () { - customPasteCommand.execute(editorState); - closeToolbar(); - }, - ), - ); - - if (!selection.isCollapsed) { - toolbarItems.add( - ContextMenuButtonItem( - label: LocaleKeys.editor_cut.tr(), - onPressed: () { - cutCommand.execute(editorState); - closeToolbar(); - }, - ), - ); - } - - toolbarItems.add( - ContextMenuButtonItem( - label: LocaleKeys.editor_select.tr(), - onPressed: () { - editorState.selectWord(offset); - closeToolbar(); - }, - ), - ); - - toolbarItems.add( - ContextMenuButtonItem( - label: LocaleKeys.editor_selectAll.tr(), - onPressed: () { - selectAllCommand.execute(editorState); - closeToolbar(); - }, - ), - ); - - return toolbarItems; -} - -extension on EditorState { - void selectWord(Offset offset) { - final node = service.selectionService.getNodeInOffset(offset); - final selection = node?.selectable?.getWordBoundaryInOffset(offset); - if (selection == null) { - return; - } - updateSelectionWithReason(selection); - } -} - -class CustomMobileFloatingToolbar extends StatelessWidget { - const CustomMobileFloatingToolbar({ - super.key, - required this.editorState, - required this.anchor, - required this.closeToolbar, - }); - - final EditorState editorState; - final Offset anchor; - final VoidCallback closeToolbar; - - @override - Widget build(BuildContext context) { - return Animate( - autoPlay: true, - effects: _getEffects(context), - child: AdaptiveTextSelectionToolbar.buttonItems( - buttonItems: buildMobileFloatingToolbarItems( - editorState, - anchor, - closeToolbar, - ), - anchors: TextSelectionToolbarAnchors( - primaryAnchor: anchor, - ), - ), - ); - } - - List _getEffects(BuildContext context) { - if (Platform.isIOS) { - final Size(:width, :height) = MediaQuery.of(context).size; - final alignmentX = (anchor.dx - width / 2) / (width / 2); - final alignmentY = (anchor.dy - height / 2) / (height / 2); - return [ - ScaleEffect( - curve: Curves.easeInOut, - alignment: Alignment(alignmentX, alignmentY), - duration: 250.milliseconds, - ), - ]; - } else if (Platform.isAndroid) { - return [ - const FadeEffect( - duration: SelectionOverlay.fadeDuration, - ), - MoveEffect( - curve: Curves.easeOutCubic, - begin: const Offset(0, 16), - duration: 100.milliseconds, - ), - ]; - } else { - return []; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart deleted file mode 100644 index f7a77a9816..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart +++ /dev/null @@ -1,69 +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 EditorStateAddBlock on EditorState { - Future insertMathEquation( - Selection selection, - ) async { - final path = selection.start.path; - final node = getNodeAtPath(path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final transaction = this.transaction; - final insertedNode = mathEquationNode(); - if (delta.isEmpty) { - transaction - ..insertNode(path, insertedNode) - ..deleteNode(node); - } else { - transaction.insertNode( - path.next, - insertedNode, - ); - } - - await apply(transaction); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final mathEquationState = getNodeAtPath(path)?.key.currentState; - if (mathEquationState != null && - mathEquationState is MathEquationBlockComponentWidgetState) { - mathEquationState.showEditingDialog(); - } - }); - } - - Future insertDivider(Selection selection) async { - // same as the [handler] of [dividerMenuItem] in Desktop - - 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; - transaction.insertNode(insertedPath, dividerNode()); - // only insert a new paragraph node when the next node is not a paragraph node - // and its delta is not empty. - final next = node.next; - if (next == null || - next.type != ParagraphBlockKeys.type || - next.delta?.isNotEmpty == true) { - transaction.insertNode( - insertedPath, - paragraphNode(), - ); - } - transaction.selectionExtraInfo = {}; - transaction.afterSelection = Selection.collapsed( - Position(path: insertedPath.next), - ); - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_screen.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_screen.dart deleted file mode 100644 index faa5795d0a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_screen.dart +++ /dev/null @@ -1,100 +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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -enum MobileBlockActionType { - delete, - duplicate, - insertAbove, - insertBelow, - color; - - static List get standard => [ - MobileBlockActionType.delete, - MobileBlockActionType.duplicate, - MobileBlockActionType.insertAbove, - MobileBlockActionType.insertBelow, - ]; - - static MobileBlockActionType fromActionString(String actionString) { - return MobileBlockActionType.values.firstWhere( - (e) => e.actionString == actionString, - orElse: () => throw Exception('Unknown action string: $actionString'), - ); - } - - String get actionString => toString(); - - FlowySvgData get icon { - return switch (this) { - MobileBlockActionType.delete => FlowySvgs.m_delete_m, - MobileBlockActionType.duplicate => FlowySvgs.m_duplicate_m, - MobileBlockActionType.insertAbove => FlowySvgs.arrow_up_s, - MobileBlockActionType.insertBelow => FlowySvgs.arrow_down_s, - MobileBlockActionType.color => FlowySvgs.m_color_m, - }; - } - - String get i18n { - return switch (this) { - MobileBlockActionType.delete => LocaleKeys.button_delete.tr(), - MobileBlockActionType.duplicate => LocaleKeys.button_duplicate.tr(), - MobileBlockActionType.insertAbove => LocaleKeys.button_insertAbove.tr(), - MobileBlockActionType.insertBelow => LocaleKeys.button_insertBelow.tr(), - MobileBlockActionType.color => - LocaleKeys.document_plugins_optionAction_color.tr(), - }; - } -} - -class MobileBlockSettingsScreen extends StatelessWidget { - const MobileBlockSettingsScreen({super.key, required this.actions}); - - final List actions; - - static const routeName = '/block_settings'; - - // the action string comes from the enum MobileBlockActionType - // example: MobileBlockActionType.delete.actionString, MobileBlockActionType.duplicate.actionString, etc. - static const supportedActions = 'actions'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - titleSpacing: 0, - title: FlowyText.semibold( - LocaleKeys.titleBar_actions.tr(), - fontSize: 14.0, - ), - leading: const AppBarBackButton(), - ), - body: SafeArea( - child: ListView.separated( - itemCount: actions.length, - itemBuilder: (context, index) { - final action = actions[index]; - return FlowyButton( - text: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 18.0, - ), - child: FlowyText(action.i18n), - ), - leftIcon: FlowySvg(action.icon), - leftIconSize: const Size.square(24), - onTap: () {}, - ); - }, - separatorBuilder: (context, index) => const Divider( - height: 1.0, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart deleted file mode 100644 index ad4d523812..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart'; -import 'package:flutter/material.dart'; - -Future showEditLinkBottomSheet( - BuildContext context, - String text, - String? href, - void Function(BuildContext context, String text, String href) onEdit, -) { - return showMobileBottomSheet( - context, - showDragHandle: true, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (context) { - return MobileBottomSheetEditLinkWidget( - text: text, - href: href, - onEdit: (text, href) => onEdit(context, text, href), - ); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart deleted file mode 100644 index b60eae3006..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension SelectionColor on EditorState { - String? getSelectionColor(String key) { - final selection = this.selection; - if (selection == null) { - return null; - } - String? color = toggledStyle[key]; - if (color == null) { - if (selection.isCollapsed && selection.startIndex != 0) { - color = getDeltaAttributeValueInSelection( - key, - selection.copyWith( - start: selection.start.copyWith( - offset: selection.startIndex - 1, - ), - ), - ); - } else { - color = getDeltaAttributeValueInSelection( - key, - ); - } - } - return color; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart deleted file mode 100644 index dccff22664..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart'; -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/mobile_toolbar_v3/util.dart'; -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'; - -const _left = 'left'; -const _center = 'center'; -const _right = 'right'; - -class AlignItems extends StatelessWidget { - AlignItems({ - super.key, - required this.editorState, - }); - - final EditorState editorState; - final List<(String, FlowySvgData)> _alignMenuItems = [ - (_left, FlowySvgs.m_aa_align_left_m), - (_center, FlowySvgs.m_aa_align_center_m), - (_right, FlowySvgs.m_aa_align_right_m), - ]; - - @override - Widget build(BuildContext context) { - final currentAlignItem = _getCurrentAlignItem(); - final theme = ToolbarColorExtension.of(context); - return PopupMenu( - itemLength: _alignMenuItems.length, - onSelected: (index) { - editorState.alignBlock( - _alignMenuItems[index].$1, - selectionExtraInfo: { - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableFloatingToolbar: true, - }, - ); - }, - menuBuilder: (context, keys, currentIndex) { - final children = _alignMenuItems - .mapIndexed( - (index, e) => [ - PopupMenuItemWrapper( - key: keys[index], - isSelected: currentIndex == index, - icon: e.$2, - ), - if (index != 0 && index != _alignMenuItems.length - 1) - const HSpace(12), - ], - ) - .flattened - .toList(); - return PopupMenuWrapper( - child: Row( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ); - }, - builder: (context, key) => MobileToolbarMenuItemWrapper( - key: key, - size: const Size(82, 52), - onTap: () async { - await editorState.alignBlock( - currentAlignItem.$1, - selectionExtraInfo: { - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableFloatingToolbar: true, - }, - ); - }, - icon: currentAlignItem.$2, - isSelected: false, - iconPadding: const EdgeInsets.symmetric( - vertical: 14.0, - ), - showDownArrow: true, - backgroundColor: theme.toolbarMenuItemBackgroundColor, - ), - ); - } - - (String, FlowySvgData) _getCurrentAlignItem() { - final align = _getCurrentBlockAlign(); - if (align == _center) { - return (_right, FlowySvgs.m_aa_align_right_s); - } else if (align == _right) { - return (_left, FlowySvgs.m_aa_align_left_s); - } else { - return (_center, FlowySvgs.m_aa_align_center_s); - } - } - - String _getCurrentBlockAlign() { - final selection = editorState.selection; - if (selection == null) { - return _left; - } - final nodes = editorState.getNodesInSelection(selection); - String? alignString; - for (final node in nodes) { - final align = node.attributes[blockComponentAlign]; - if (alignString == null) { - alignString = align; - } else if (alignString != align) { - return _left; - } - } - return alignString ?? _left; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart deleted file mode 100644 index 0de86ffd6c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart +++ /dev/null @@ -1,82 +0,0 @@ -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/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -class BIUSItems extends StatelessWidget { - BIUSItems({ - super.key, - required this.editorState, - }); - - final EditorState editorState; - - final List<(FlowySvgData, String)> _bius = [ - (FlowySvgs.m_toolbar_bold_m, AppFlowyRichTextKeys.bold), - (FlowySvgs.m_toolbar_italic_m, AppFlowyRichTextKeys.italic), - (FlowySvgs.m_toolbar_underline_m, AppFlowyRichTextKeys.underline), - (FlowySvgs.m_toolbar_strike_m, AppFlowyRichTextKeys.strikethrough), - ]; - - @override - Widget build(BuildContext context) { - return IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: _bius - .mapIndexed( - (index, e) => [ - _buildBIUSItem( - context, - index, - e.$1, - e.$2, - ), - if (index != 0 || index != _bius.length - 1) - const ScaledVerticalDivider(), - ], - ) - .flattened - .toList(), - ), - ); - } - - Widget _buildBIUSItem( - BuildContext context, - int index, - FlowySvgData icon, - String richTextKey, - ) { - final theme = ToolbarColorExtension.of(context); - return StatefulBuilder( - builder: (_, setState) => MobileToolbarMenuItemWrapper( - size: const Size(62, 52), - enableTopLeftRadius: index == 0, - enableBottomLeftRadius: index == 0, - enableTopRightRadius: index == _bius.length - 1, - enableBottomRightRadius: index == _bius.length - 1, - backgroundColor: theme.toolbarMenuItemBackgroundColor, - onTap: () async { - await editorState.toggleAttribute( - richTextKey, - selectionExtraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ); - // refresh the status - setState(() {}); - }, - icon: icon, - isSelected: editorState.isTextDecorationSelected(richTextKey) && - editorState.toggledStyle[richTextKey] != false, - iconPadding: const EdgeInsets.symmetric( - vertical: 14.0, - ), - ), - ); - } -} 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 deleted file mode 100644 index 8e1a8533e0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart +++ /dev/null @@ -1,219 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart'; -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:collection/collection.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class BlockItems extends StatelessWidget { - BlockItems({ - super.key, - required this.service, - required this.editorState, - }); - - final EditorState editorState; - final AppFlowyMobileToolbarWidgetService service; - - final List<(FlowySvgData, String)> _blockItems = [ - (FlowySvgs.m_toolbar_bulleted_list_m, BulletedListBlockKeys.type), - (FlowySvgs.m_toolbar_numbered_list_m, NumberedListBlockKeys.type), - (FlowySvgs.m_aa_quote_m, QuoteBlockKeys.type), - ]; - - @override - Widget build(BuildContext context) { - return IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ..._blockItems - .mapIndexed( - (index, e) => [ - _buildBlockItem( - context, - index, - e.$1, - e.$2, - ), - if (index != 0) const ScaledVerticalDivider(), - ], - ) - .flattened, - // this item is a special case, use link item here instead of block item - _buildLinkItem(context), - ], - ), - ); - } - - Widget _buildBlockItem( - BuildContext context, - int index, - FlowySvgData icon, - String blockType, - ) { - final theme = ToolbarColorExtension.of(context); - return MobileToolbarMenuItemWrapper( - size: const Size(62, 54), - enableTopLeftRadius: index == 0, - enableBottomLeftRadius: index == 0, - enableTopRightRadius: false, - enableBottomRightRadius: false, - onTap: () async { - await _convert(blockType); - }, - backgroundColor: theme.toolbarMenuItemBackgroundColor, - icon: icon, - isSelected: editorState.isBlockTypeSelected(blockType), - iconPadding: const EdgeInsets.symmetric( - vertical: 14.0, - ), - ); - } - - Widget _buildLinkItem(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - final items = [ - (AppFlowyRichTextKeys.code, FlowySvgs.m_aa_code_m), - // (InlineMathEquationKeys.formula, FlowySvgs.m_aa_math_s), - ]; - return PopupMenu( - itemLength: items.length, - onSelected: (index) async { - await editorState.toggleAttribute(items[index].$1); - }, - menuBuilder: (context, keys, currentIndex) { - final children = items - .mapIndexed( - (index, e) => [ - PopupMenuItemWrapper( - key: keys[index], - isSelected: currentIndex == index, - icon: e.$2, - ), - if (index != 0 || index != items.length - 1) const HSpace(12), - ], - ) - .flattened - .toList(); - return PopupMenuWrapper( - child: Row( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ); - }, - builder: (context, key) => MobileToolbarMenuItemWrapper( - key: key, - size: const Size(62, 54), - enableTopLeftRadius: false, - enableBottomLeftRadius: false, - showDownArrow: true, - onTap: _onLinkItemTap, - backgroundColor: theme.toolbarMenuItemBackgroundColor, - icon: FlowySvgs.m_toolbar_link_m, - isSelected: false, - iconPadding: const EdgeInsets.symmetric( - vertical: 14.0, - ), - ), - ); - } - - void _onLinkItemTap() async { - final selection = editorState.selection; - if (selection == null) { - return; - } - final nodes = editorState.getNodesInSelection(selection); - // show edit link bottom sheet - final context = nodes.firstOrNull?.context; - if (context != null) { - _closeKeyboard(selection); - - // keep the selection - unawaited( - editorState.updateSelectionWithReason( - selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableFloatingToolbar: true, - }, - ), - ); - keepEditorFocusNotifier.increase(); - - final text = editorState - .getTextInSelection( - selection, - ) - .join(); - final href = editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.href, - selection, - ); - await showEditLinkBottomSheet( - context, - text, - href, - (context, newText, newHref) { - editorState.updateTextAndHref( - text, - href, - newText, - newHref, - selection: selection, - ); - context.pop(true); - }, - ); - // re-open the keyboard again - unawaited( - editorState.updateSelectionWithReason( - selection, - extraInfo: {}, - ), - ); - } - } - - void _closeKeyboard(Selection selection) { - editorState.updateSelectionWithReason( - selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ); - editorState.service.keyboardService?.closeKeyboard(); - } - - Future _convert(String blockType) async { - await editorState.convertBlockType( - blockType, - selectionExtraInfo: { - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableFloatingToolbar: true, - }, - ); - unawaited( - editorState.updateSelectionWithReason( - editorState.selection, - extraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart deleted file mode 100644 index 4c91a00bc7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart +++ /dev/null @@ -1,26 +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 CloseKeyboardOrMenuButton extends StatelessWidget { - const CloseKeyboardOrMenuButton({ - super.key, - required this.onPressed, - }); - - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 62, - height: 42, - child: FlowyButton( - text: const FlowySvg( - FlowySvgs.m_toolbar_keyboard_m, - ), - onTap: onPressed, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart deleted file mode 100644 index 997faaf2a9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.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/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'; -import 'package:flutter/material.dart'; - -class ColorItem extends StatelessWidget { - const ColorItem({ - super.key, - required this.editorState, - required this.service, - }); - - final EditorState editorState; - final AppFlowyMobileToolbarWidgetService service; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - final String? selectedTextColor = - editorState.getSelectionColor(AppFlowyRichTextKeys.textColor); - final String? selectedBackgroundColor = - editorState.getSelectionColor(AppFlowyRichTextKeys.backgroundColor); - final backgroundColor = EditorFontColors.fromBuiltInColors( - context, - selectedBackgroundColor?.tryToColor(), - ); - return MobileToolbarMenuItemWrapper( - size: const Size(82, 52), - onTap: () async { - service.closeKeyboard(); - unawaited( - editorState.updateSelectionWithReason( - editorState.selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ), - ); - keepEditorFocusNotifier.increase(); - await showTextColorAndBackgroundColorPicker( - context, - editorState: editorState, - selection: editorState.selection!, - ); - }, - icon: FlowySvgs.m_aa_font_color_m, - iconColor: EditorFontColors.fromBuiltInColors( - context, - selectedTextColor?.tryToColor(), - ), - backgroundColor: backgroundColor ?? theme.toolbarMenuItemBackgroundColor, - selectedBackgroundColor: backgroundColor, - isSelected: selectedBackgroundColor != null, - showRightArrow: true, - iconPadding: const EdgeInsets.only( - top: 14.0, - bottom: 14.0, - right: 28.0, - ), - ); - } -} 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 deleted file mode 100644 index 6ec777429c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart +++ /dev/null @@ -1,303 +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/plugins/document/presentation/editor_plugins/base/font_colors.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.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: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'; - -const _count = 6; - -Future showTextColorAndBackgroundColorPicker( - BuildContext context, { - required EditorState editorState, - required Selection selection, -}) async { - final theme = ToolbarColorExtension.of(context); - await showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showDoneButton: true, - barrierColor: Colors.transparent, - backgroundColor: theme.toolbarMenuBackgroundColor, - elevation: 20, - title: LocaleKeys.grid_selectOption_colorPanelTitle.tr(), - padding: const EdgeInsets.fromLTRB(10, 4, 10, 8), - builder: (context) { - return _TextColorAndBackgroundColor( - editorState: editorState, - selection: selection, - ); - }, - ); - Future.delayed(const Duration(milliseconds: 100), () { - // highlight the selected text again. - editorState.updateSelectionWithReason( - selection, - extraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - }, - ); - }); -} - -class _TextColorAndBackgroundColor extends StatefulWidget { - const _TextColorAndBackgroundColor({ - required this.editorState, - required this.selection, - }); - - final EditorState editorState; - final Selection selection; - - @override - State<_TextColorAndBackgroundColor> createState() => - _TextColorAndBackgroundColorState(); -} - -class _TextColorAndBackgroundColorState - extends State<_TextColorAndBackgroundColor> { - @override - Widget build(BuildContext context) { - final String? selectedTextColor = - widget.editorState.getSelectionColor(AppFlowyRichTextKeys.textColor); - final String? selectedBackgroundColor = widget.editorState - .getSelectionColor(AppFlowyRichTextKeys.backgroundColor); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 20, - left: 6.0, - ), - child: FlowyText( - LocaleKeys.editor_textColor.tr(), - fontSize: 14.0, - ), - ), - const VSpace(6.0), - EditorTextColorWidget( - selectedColor: selectedTextColor?.tryToColor(), - onSelectedColor: (textColor) async { - final hex = textColor.a == 0 ? null : textColor.toHex(); - final selection = widget.selection; - if (selection.isCollapsed) { - widget.editorState.updateToggledStyle( - AppFlowyRichTextKeys.textColor, - hex ?? '', - ); - } else { - await widget.editorState.formatDelta( - widget.selection, - { - AppFlowyRichTextKeys.textColor: hex, - }, - selectionExtraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ); - } - setState(() {}); - }, - ), - Padding( - padding: const EdgeInsets.only( - top: 18.0, - left: 6.0, - ), - child: FlowyText( - LocaleKeys.editor_backgroundColor.tr(), - fontSize: 14.0, - ), - ), - const VSpace(6.0), - EditorBackgroundColors( - selectedColor: selectedBackgroundColor?.tryToColor(), - onSelectedColor: (backgroundColor) async { - final hex = backgroundColor.a == 0 ? null : backgroundColor.toHex(); - final selection = widget.selection; - if (selection.isCollapsed) { - widget.editorState.updateToggledStyle( - AppFlowyRichTextKeys.backgroundColor, - hex ?? '', - ); - } else { - await widget.editorState.formatDelta( - widget.selection, - { - AppFlowyRichTextKeys.backgroundColor: hex, - }, - selectionExtraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ); - } - setState(() {}); - }, - ), - ], - ); - } -} - -class EditorBackgroundColors extends StatelessWidget { - const EditorBackgroundColors({ - super.key, - this.selectedColor, - required this.onSelectedColor, - }); - - final Color? selectedColor; - final void Function(Color color) onSelectedColor; - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).brightness == Brightness.light - ? EditorFontColors.lightColors - : EditorFontColors.darkColors; - return GridView.count( - crossAxisCount: _count, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: colors.mapIndexed( - (index, color) { - return _BackgroundColorItem( - color: color, - isSelected: - selectedColor == null ? index == 0 : selectedColor == color, - onTap: () => onSelectedColor(color), - ); - }, - ).toList(), - ); - } -} - -class _BackgroundColorItem extends StatelessWidget { - const _BackgroundColorItem({ - required this.color, - required this.isSelected, - required this.onTap, - }); - - final VoidCallback onTap; - final Color color; - final bool isSelected; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - return GestureDetector( - onTap: onTap, - child: Container( - margin: const EdgeInsets.all(6.0), - decoration: BoxDecoration( - color: color, - borderRadius: Corners.s12Border, - border: Border.all( - width: isSelected ? 2.0 : 1.0, - color: isSelected - ? theme.toolbarMenuItemSelectedBackgroundColor - : Theme.of(context).dividerColor, - ), - ), - alignment: Alignment.center, - child: isSelected - ? const FlowySvg( - FlowySvgs.m_blue_check_s, - size: Size.square(28.0), - blendMode: null, - ) - : null, - ), - ); - } -} - -class EditorTextColorWidget extends StatelessWidget { - EditorTextColorWidget({ - super.key, - this.selectedColor, - required this.onSelectedColor, - }); - - final Color? selectedColor; - final void Function(Color color) onSelectedColor; - - final colors = [ - const Color(0x00FFFFFF), - const Color(0xFFDB3636), - const Color(0xFFEA8F06), - const Color(0xFF18A166), - const Color(0xFF205EEE), - const Color(0xFFC619C9), - ]; - - @override - Widget build(BuildContext context) { - return GridView.count( - crossAxisCount: _count, - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - children: colors.mapIndexed( - (index, color) { - return _TextColorItem( - color: color, - isSelected: - selectedColor == null ? index == 0 : selectedColor == color, - onTap: () => onSelectedColor(color), - ); - }, - ).toList(), - ); - } -} - -class _TextColorItem extends StatelessWidget { - const _TextColorItem({ - required this.color, - required this.isSelected, - required this.onTap, - }); - - final VoidCallback onTap; - final Color color; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - margin: const EdgeInsets.all(6.0), - decoration: BoxDecoration( - borderRadius: Corners.s12Border, - border: Border.all( - width: isSelected ? 2.0 : 1.0, - color: isSelected - ? const Color(0xff00C6F1) - : Theme.of(context).dividerColor, - ), - ), - alignment: Alignment.center, - child: FlowyText( - 'A', - fontSize: 24, - color: color.a == 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 deleted file mode 100644 index 96431996f5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.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/google_fonts_extension.dart'; -import 'package:appflowy/util/font_family_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; - -class FontFamilyItem extends StatelessWidget { - const FontFamilyItem({ - super.key, - required this.editorState, - }); - - final EditorState editorState; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - final fontFamily = _getCurrentSelectedFontFamilyName(); - final systemFonFamily = - context.read().state.fontFamily; - return MobileToolbarMenuItemWrapper( - size: const Size(144, 52), - onTap: () async { - final selection = editorState.selection; - if (selection == null) { - return; - } - // disable the floating toolbar - unawaited( - editorState.updateSelectionWithReason( - selection, - extraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDisableMobileToolbarKey: true, - }, - ), - ); - - final newFont = await context - .read() - .push(FontPickerScreen.routeName); - - // if the selection is not collapsed, apply the font to the selection. - if (newFont != null && !selection.isCollapsed) { - if (newFont != fontFamily) { - await editorState.formatDelta(selection, { - AppFlowyRichTextKeys.fontFamily: newFont, - }); - } - } - - // wait for the font picker screen to be dismissed. - Future.delayed(const Duration(milliseconds: 250), () { - // highlight the selected text again. - editorState.updateSelectionWithReason( - selection, - extraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDisableMobileToolbarKey: false, - }, - ); - // if the selection is collapsed, save the font for the next typing. - if (newFont != null && selection.isCollapsed) { - editorState.updateToggledStyle( - AppFlowyRichTextKeys.fontFamily, - getGoogleFontSafely(newFont).fontFamily, - ); - } - }); - }, - text: (fontFamily ?? systemFonFamily).fontFamilyDisplayName, - fontFamily: fontFamily ?? systemFonFamily, - backgroundColor: theme.toolbarMenuItemBackgroundColor, - isSelected: false, - enable: true, - showRightArrow: true, - iconPadding: const EdgeInsets.only( - top: 14.0, - bottom: 14.0, - left: 14.0, - right: 12.0, - ), - textPadding: const EdgeInsets.only( - right: 16.0, - ), - ); - } - - String? _getCurrentSelectedFontFamilyName() { - final toggleFontFamily = - editorState.toggledStyle[AppFlowyRichTextKeys.fontFamily]; - if (toggleFontFamily is String && toggleFontFamily.isNotEmpty) { - return toggleFontFamily; - } - final selection = editorState.selection; - if (selection != null && - selection.isCollapsed && - selection.startIndex != 0) { - return editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.fontFamily, - selection.copyWith( - start: selection.start.copyWith( - offset: selection.startIndex - 1, - ), - ), - ); - } - return editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.fontFamily, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart deleted file mode 100644 index b98a6fddff..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -class HeadingsAndTextItems extends StatelessWidget { - const HeadingsAndTextItems({ - super.key, - required this.editorState, - }); - - final EditorState editorState; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _HeadingOrTextItem( - icon: FlowySvgs.m_aa_h1_m, - blockType: HeadingBlockKeys.type, - editorState: editorState, - level: 1, - ), - _HeadingOrTextItem( - icon: FlowySvgs.m_aa_h2_m, - blockType: HeadingBlockKeys.type, - editorState: editorState, - level: 2, - ), - _HeadingOrTextItem( - icon: FlowySvgs.m_aa_h3_m, - blockType: HeadingBlockKeys.type, - editorState: editorState, - level: 3, - ), - _HeadingOrTextItem( - icon: FlowySvgs.m_aa_paragraph_m, - blockType: ParagraphBlockKeys.type, - editorState: editorState, - ), - ], - ); - } -} - -class _HeadingOrTextItem extends StatelessWidget { - const _HeadingOrTextItem({ - required this.icon, - required this.blockType, - required this.editorState, - this.level, - }); - - final FlowySvgData icon; - final String blockType; - final EditorState editorState; - final int? level; - - @override - Widget build(BuildContext context) { - final isSelected = editorState.isBlockTypeSelected( - blockType, - level: level, - ); - final padding = level != null - ? EdgeInsets.symmetric( - vertical: 14.0 - (3 - level!) * 3.0, - ) - : const EdgeInsets.symmetric( - vertical: 16.0, - ); - return MobileToolbarMenuItemWrapper( - size: const Size(76, 52), - onTap: () async => _convert(isSelected), - icon: icon, - isSelected: isSelected, - iconPadding: padding, - ); - } - - Future _convert(bool isSelected) async { - await editorState.convertBlockType( - blockType, - isSelected: isSelected, - extraAttributes: level != null - ? { - HeadingBlockKeys.level: level!, - } - : null, - selectionExtraInfo: { - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableFloatingToolbar: true, - }, - ); - unawaited( - editorState.updateSelectionWithReason( - editorState.selection, - extraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart deleted file mode 100644 index 2ddcd4dacb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart +++ /dev/null @@ -1,56 +0,0 @@ -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/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -class IndentAndOutdentItems extends StatelessWidget { - const IndentAndOutdentItems({ - super.key, - required this.service, - required this.editorState, - }); - - final EditorState editorState; - final AppFlowyMobileToolbarWidgetService service; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - return IntrinsicHeight( - child: Row( - children: [ - MobileToolbarMenuItemWrapper( - size: const Size(95, 52), - icon: FlowySvgs.m_aa_outdent_m, - enable: isOutdentable(editorState), - isSelected: false, - enableTopRightRadius: false, - enableBottomRightRadius: false, - iconPadding: const EdgeInsets.symmetric(vertical: 14.0), - backgroundColor: theme.toolbarMenuItemBackgroundColor, - onTap: () { - service.closeItemMenu(); - outdentCommand.execute(editorState); - }, - ), - const ScaledVerticalDivider(), - MobileToolbarMenuItemWrapper( - size: const Size(95, 52), - icon: FlowySvgs.m_aa_indent_m, - enable: isIndentable(editorState), - isSelected: false, - enableTopLeftRadius: false, - enableBottomLeftRadius: false, - iconPadding: const EdgeInsets.symmetric(vertical: 14.0), - backgroundColor: theme.toolbarMenuItemBackgroundColor, - onTap: () { - service.closeItemMenu(); - indentCommand.execute(editorState); - }, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart deleted file mode 100644 index 7464514f93..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; -import 'package:flowy_svg/flowy_svg.dart'; -import 'package:flutter/material.dart'; - -class PopupMenuItemWrapper extends StatelessWidget { - const PopupMenuItemWrapper({ - super.key, - required this.isSelected, - required this.icon, - }); - - final bool isSelected; - final FlowySvgData icon; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - return Container( - width: 62, - height: 44, - decoration: ShapeDecoration( - color: isSelected ? theme.toolbarMenuItemSelectedBackgroundColor : null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 9), - child: FlowySvg( - icon, - color: isSelected - ? theme.toolbarMenuIconSelectedColor - : theme.toolbarMenuIconColor, - ), - ); - } -} - -class PopupMenuWrapper extends StatelessWidget { - const PopupMenuWrapper({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - return Container( - height: 64, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - decoration: ShapeDecoration( - color: theme.toolbarMenuBackgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - shadows: [ - BoxShadow( - color: theme.toolbarShadowColor, - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart deleted file mode 100644 index d678d7c0ba..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class PopupMenu extends StatefulWidget { - const PopupMenu({ - super.key, - required this.onSelected, - required this.itemLength, - required this.menuBuilder, - required this.builder, - }); - - final Widget Function(BuildContext context, Key key) builder; - final int itemLength; - final Widget Function( - BuildContext context, - List keys, - int currentIndex, - ) menuBuilder; - final void Function(int index) onSelected; - - @override - State createState() => _PopupMenuState(); -} - -class _PopupMenuState extends State { - final key = GlobalKey(); - final indexNotifier = ValueNotifier(-1); - late List itemKeys; - - OverlayEntry? popupMenuOverlayEntry; - - Rect get rect { - final RenderBox renderBox = - key.currentContext!.findRenderObject() as RenderBox; - final size = renderBox.size; - final offset = renderBox.localToGlobal(Offset.zero); - return Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height); - } - - @override - void initState() { - super.initState(); - - indexNotifier.value = widget.itemLength - 1; - itemKeys = List.generate( - widget.itemLength, - (_) => GlobalKey(), - ); - - indexNotifier.addListener(HapticFeedback.mediumImpact); - } - - @override - void dispose() { - indexNotifier.removeListener(HapticFeedback.mediumImpact); - indexNotifier.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onLongPressStart: (details) { - _showMenu(context); - }, - onLongPressMoveUpdate: (details) { - _updateSelection(details); - }, - onLongPressCancel: () { - _hideMenu(); - }, - onLongPressUp: () { - if (indexNotifier.value != -1) { - widget.onSelected(indexNotifier.value); - } - _hideMenu(); - }, - child: widget.builder(context, key), - ); - } - - void _showMenu(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - _hideMenu(); - - indexNotifier.value = widget.itemLength - 1; - popupMenuOverlayEntry ??= OverlayEntry( - builder: (context) { - final screenSize = MediaQuery.of(context).size; - final right = screenSize.width - rect.right; - final bottom = screenSize.height - rect.top + 16; - return Positioned( - right: right, - bottom: bottom, - child: ColoredBox( - color: theme.toolbarMenuBackgroundColor, - child: ValueListenableBuilder( - valueListenable: indexNotifier, - builder: (context, value, _) => widget.menuBuilder( - context, - itemKeys, - value, - ), - ), - ), - ); - }, - ); - Overlay.of(context).insert(popupMenuOverlayEntry!); - } - - void _hideMenu() { - indexNotifier.value = -1; - - popupMenuOverlayEntry?.remove(); - popupMenuOverlayEntry = null; - } - - void _updateSelection(LongPressMoveUpdateDetails details) { - final dx = details.globalPosition.dx; - for (var i = 0; i < itemKeys.length; i++) { - final key = itemKeys[i]; - final RenderBox renderBox = - key.currentContext!.findRenderObject() as RenderBox; - final size = renderBox.size; - final offset = renderBox.localToGlobal(Offset.zero); - final rect = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height); - // ignore the position overflow - if ((i == 0 && dx < rect.left) || - (i == itemKeys.length - 1 && dx > rect.right)) { - indexNotifier.value = -1; - break; - } - if (rect.left <= dx && dx <= rect.right) { - indexNotifier.value = itemKeys.indexOf(key); - break; - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart deleted file mode 100644 index d35b8d56df..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart +++ /dev/null @@ -1,173 +0,0 @@ -// workaround for toolbar theme color. - -import 'package:flutter/material.dart'; - -class ToolbarColorExtension extends ThemeExtension { - factory ToolbarColorExtension.light() => const ToolbarColorExtension( - toolbarBackgroundColor: Color(0xFFFFFFFF), - toolbarItemIconColor: Color(0xFF1F2329), - toolbarItemIconDisabledColor: Color(0xFF999BA0), - toolbarItemIconSelectedColor: Color(0x1F232914), - toolbarItemSelectedBackgroundColor: Color(0xFFF2F2F2), - toolbarMenuBackgroundColor: Color(0xFFFFFFFF), - toolbarMenuItemBackgroundColor: Color(0xFFF2F2F7), - toolbarMenuItemSelectedBackgroundColor: Color(0xFF00BCF0), - toolbarMenuIconColor: Color(0xFF1F2329), - toolbarMenuIconDisabledColor: Color(0xFF999BA0), - toolbarMenuIconSelectedColor: Color(0xFFFFFFFF), - toolbarShadowColor: Color(0x2D000000), - ); - - factory ToolbarColorExtension.dark() => const ToolbarColorExtension( - toolbarBackgroundColor: Color(0xFF1F2329), - toolbarItemIconColor: Color(0xFFF3F3F8), - toolbarItemIconDisabledColor: Color(0xFF55565B), - toolbarItemIconSelectedColor: Color(0xFF00BCF0), - toolbarItemSelectedBackgroundColor: Color(0xFF3A3D43), - toolbarMenuBackgroundColor: Color(0xFF23262B), - toolbarMenuItemBackgroundColor: Color(0xFF2D3036), - toolbarMenuItemSelectedBackgroundColor: Color(0xFF00BCF0), - toolbarMenuIconColor: Color(0xFFF3F3F8), - toolbarMenuIconDisabledColor: Color(0xFF55565B), - toolbarMenuIconSelectedColor: Color(0xFF1F2329), - toolbarShadowColor: Color.fromARGB(80, 112, 112, 112), - ); - - factory ToolbarColorExtension.fromBrightness(Brightness brightness) => - brightness == Brightness.light - ? ToolbarColorExtension.light() - : ToolbarColorExtension.dark(); - - const ToolbarColorExtension({ - required this.toolbarBackgroundColor, - required this.toolbarItemIconColor, - required this.toolbarItemIconDisabledColor, - required this.toolbarItemIconSelectedColor, - required this.toolbarMenuBackgroundColor, - required this.toolbarMenuItemBackgroundColor, - required this.toolbarMenuItemSelectedBackgroundColor, - required this.toolbarItemSelectedBackgroundColor, - required this.toolbarMenuIconColor, - required this.toolbarMenuIconDisabledColor, - required this.toolbarMenuIconSelectedColor, - required this.toolbarShadowColor, - }); - - final Color toolbarBackgroundColor; - - final Color toolbarItemIconColor; - final Color toolbarItemIconDisabledColor; - final Color toolbarItemIconSelectedColor; - final Color toolbarItemSelectedBackgroundColor; - - final Color toolbarMenuBackgroundColor; - final Color toolbarMenuItemBackgroundColor; - final Color toolbarMenuItemSelectedBackgroundColor; - final Color toolbarMenuIconColor; - final Color toolbarMenuIconDisabledColor; - final Color toolbarMenuIconSelectedColor; - - final Color toolbarShadowColor; - - static ToolbarColorExtension of(BuildContext context) { - return Theme.of(context).extension()!; - } - - @override - ToolbarColorExtension copyWith({ - Color? toolbarBackgroundColor, - Color? toolbarItemIconColor, - Color? toolbarItemIconDisabledColor, - Color? toolbarItemIconSelectedColor, - Color? toolbarMenuBackgroundColor, - Color? toolbarItemSelectedBackgroundColor, - Color? toolbarMenuItemBackgroundColor, - Color? toolbarMenuItemSelectedBackgroundColor, - Color? toolbarMenuIconColor, - Color? toolbarMenuIconDisabledColor, - Color? toolbarMenuIconSelectedColor, - Color? toolbarShadowColor, - }) { - return ToolbarColorExtension( - toolbarBackgroundColor: - toolbarBackgroundColor ?? this.toolbarBackgroundColor, - toolbarItemIconColor: toolbarItemIconColor ?? this.toolbarItemIconColor, - toolbarItemIconDisabledColor: - toolbarItemIconDisabledColor ?? this.toolbarItemIconDisabledColor, - toolbarItemIconSelectedColor: - toolbarItemIconSelectedColor ?? this.toolbarItemIconSelectedColor, - toolbarItemSelectedBackgroundColor: toolbarItemSelectedBackgroundColor ?? - this.toolbarItemSelectedBackgroundColor, - toolbarMenuBackgroundColor: - toolbarMenuBackgroundColor ?? this.toolbarMenuBackgroundColor, - toolbarMenuItemBackgroundColor: - toolbarMenuItemBackgroundColor ?? this.toolbarMenuItemBackgroundColor, - toolbarMenuItemSelectedBackgroundColor: - toolbarMenuItemSelectedBackgroundColor ?? - this.toolbarMenuItemSelectedBackgroundColor, - toolbarMenuIconColor: toolbarMenuIconColor ?? this.toolbarMenuIconColor, - toolbarMenuIconDisabledColor: - toolbarMenuIconDisabledColor ?? this.toolbarMenuIconDisabledColor, - toolbarMenuIconSelectedColor: - toolbarMenuIconSelectedColor ?? this.toolbarMenuIconSelectedColor, - toolbarShadowColor: toolbarShadowColor ?? this.toolbarShadowColor, - ); - } - - @override - ToolbarColorExtension lerp(ToolbarColorExtension? other, double t) { - if (other is! ToolbarColorExtension) { - return this; - } - return ToolbarColorExtension( - toolbarBackgroundColor: - Color.lerp(toolbarBackgroundColor, other.toolbarBackgroundColor, t)!, - toolbarItemIconColor: - Color.lerp(toolbarItemIconColor, other.toolbarItemIconColor, t)!, - toolbarItemIconDisabledColor: Color.lerp( - toolbarItemIconDisabledColor, - other.toolbarItemIconDisabledColor, - t, - )!, - toolbarItemIconSelectedColor: Color.lerp( - toolbarItemIconSelectedColor, - other.toolbarItemIconSelectedColor, - t, - )!, - toolbarItemSelectedBackgroundColor: Color.lerp( - toolbarItemSelectedBackgroundColor, - other.toolbarItemSelectedBackgroundColor, - t, - )!, - toolbarMenuBackgroundColor: Color.lerp( - toolbarMenuBackgroundColor, - other.toolbarMenuBackgroundColor, - t, - )!, - toolbarMenuItemBackgroundColor: Color.lerp( - toolbarMenuItemBackgroundColor, - other.toolbarMenuItemBackgroundColor, - t, - )!, - toolbarMenuItemSelectedBackgroundColor: Color.lerp( - toolbarMenuItemSelectedBackgroundColor, - other.toolbarMenuItemSelectedBackgroundColor, - t, - )!, - toolbarMenuIconColor: - Color.lerp(toolbarMenuIconColor, other.toolbarMenuIconColor, t)!, - toolbarMenuIconDisabledColor: Color.lerp( - toolbarMenuIconDisabledColor, - other.toolbarMenuIconDisabledColor, - t, - )!, - toolbarMenuIconSelectedColor: Color.lerp( - toolbarMenuIconSelectedColor, - other.toolbarMenuIconSelectedColor, - t, - )!, - toolbarShadowColor: - Color.lerp(toolbarShadowColor, other.toolbarShadowColor, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart deleted file mode 100644 index 7489911fb7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.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'; -import 'package:flutter/material.dart'; - -final aaToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, service, onMenu, _) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - isSelected: () => service.showMenuNotifier.value, - keepSelectedStatus: true, - icon: FlowySvgs.m_toolbar_aa_m, - onTap: () => onMenu?.call(), - ); - }, - menuBuilder: (context, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return const SizedBox.shrink(); - } - return _TextDecorationMenu( - editorState, - selection, - service, - ); - }, -); - -class _TextDecorationMenu extends StatefulWidget { - const _TextDecorationMenu( - this.editorState, - this.selection, - this.service, - ); - - final EditorState editorState; - final Selection selection; - final AppFlowyMobileToolbarWidgetService service; - - @override - State<_TextDecorationMenu> createState() => _TextDecorationMenuState(); -} - -class _TextDecorationMenuState extends State<_TextDecorationMenu> { - EditorState get editorState => widget.editorState; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - return ColoredBox( - color: theme.toolbarMenuBackgroundColor, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only( - top: 16, - bottom: 20, - left: 12, - right: 12, - ) * - context.scale, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - HeadingsAndTextItems( - editorState: editorState, - ), - const ScaledVSpace(), - Row( - children: [ - BIUSItems( - editorState: editorState, - ), - const Spacer(), - ColorItem( - editorState: editorState, - service: widget.service, - ), - ], - ), - const ScaledVSpace(), - Row( - children: [ - BlockItems( - service: widget.service, - editorState: editorState, - ), - const Spacer(), - AlignItems( - editorState: editorState, - ), - ], - ), - const ScaledVSpace(), - Row( - children: [ - FontFamilyItem( - editorState: editorState, - ), - const Spacer(), - IndentAndOutdentItems( - service: widget.service, - editorState: editorState, - ), - ], - ), - ], - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index c09368ff95..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart +++ /dev/null @@ -1,99 +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/mobile/presentation/bottom_sheet/bottom_sheet.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/tasks/app_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.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'); - -final addBlockToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, service, __, onAction) { - return AppFlowyMobileToolbarIconItem( - key: addBlockToolbarItemKey, - editorState: editorState, - icon: FlowySvgs.m_toolbar_add_m, - onTap: () { - 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 didAddBlock = await showAddBlockMenu( - AppGlobals.rootNavKey.currentContext!, - editorState: editorState, - selection: selection!, - ); - if (didAddBlock != true) { - unawaited(editorState.updateSelectionWithReason(selection)); - } - }); - }, - ); - }, -); - -Future showAddBlockMenu( - BuildContext context, { - required EditorState editorState, - required Selection selection, -}) async => - showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showCloseButton: true, - title: LocaleKeys.button_add.tr(), - barrierColor: Colors.transparent, - backgroundColor: - ToolbarColorExtension.of(context).toolbarMenuBackgroundColor, - elevation: 20, - enableDraggableScrollable: true, - builder: (_) => Padding( - padding: EdgeInsets.all(16 * context.scale), - child: AddBlockMenu(selection: selection, editorState: editorState), - ), - ); - -class AddBlockMenu extends StatelessWidget { - const AddBlockMenu({ - super.key, - required this.selection, - required this.editorState, - }); - - final Selection selection; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - final builder = AddBlockMenuItemBuilder( - editorState: editorState, - selection: selection, - ); - return TypeOptionMenu( - values: builder.buildTypeOptionMenuItemValues(context), - scaleFactor: context.scale, - ); - } -} 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 deleted file mode 100644 index 787ccfda9f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart +++ /dev/null @@ -1,592 +0,0 @@ -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'; -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_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart'; -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'; -import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -abstract class AppFlowyMobileToolbarWidgetService { - void closeItemMenu(); - - void closeKeyboard(); - - PropertyValueNotifier get showMenuNotifier; -} - -class AppFlowyMobileToolbar extends StatefulWidget { - const AppFlowyMobileToolbar({ - super.key, - this.toolbarHeight = 50.0, - required this.editorState, - required this.toolbarItemsBuilder, - required this.child, - }); - - final EditorState editorState; - final double toolbarHeight; - final List Function( - Selection? selection, - ) toolbarItemsBuilder; - final Widget child; - - @override - State createState() => _AppFlowyMobileToolbarState(); -} - -class _AppFlowyMobileToolbarState extends State { - OverlayEntry? toolbarOverlay; - - final isKeyboardShow = ValueNotifier(false); - - @override - void initState() { - super.initState(); - - _insertKeyboardToolbar(); - KeyboardHeightObserver.instance.addListener(_onKeyboardHeightChanged); - } - - @override - void dispose() { - _removeKeyboardToolbar(); - KeyboardHeightObserver.instance.removeListener(_onKeyboardHeightChanged); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded(child: widget.child), - // add a bottom offset to make sure the toolbar is above the keyboard - ValueListenableBuilder( - valueListenable: isKeyboardShow, - builder: (context, isKeyboardShow, __) { - return SizedBox( - // only adding padding when the keyboard is triggered by editor - height: isKeyboardShow && widget.editorState.selection != null - ? widget.toolbarHeight - : 0, - ); - }, - ), - ], - ); - } - - void _onKeyboardHeightChanged(double height) { - isKeyboardShow.value = height > 0; - } - - void _removeKeyboardToolbar() { - toolbarOverlay?.remove(); - toolbarOverlay?.dispose(); - toolbarOverlay = null; - } - - void _insertKeyboardToolbar() { - _removeKeyboardToolbar(); - - Widget child = ValueListenableBuilder( - valueListenable: widget.editorState.selectionNotifier, - builder: (_, Selection? selection, __) { - // if the selection is null, hide the toolbar - if (selection == null || - widget.editorState.selectionExtraInfo?[ - selectionExtraInfoDisableMobileToolbarKey] == - true) { - return const SizedBox.shrink(); - } - - return RepaintBoundary( - child: BlocProvider.value( - value: context.read(), - child: _MobileToolbar( - editorState: widget.editorState, - toolbarItems: widget.toolbarItemsBuilder(selection), - toolbarHeight: widget.toolbarHeight, - ), - ), - ); - }, - ); - - child = Stack( - children: [ - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Material( - child: child, - ), - ), - ], - ); - - final router = GoRouter.of(context); - - toolbarOverlay = OverlayEntry( - builder: (context) { - return Provider.value( - value: router, - child: child, - ); - }, - ); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Overlay.of(context, rootOverlay: true).insert(toolbarOverlay!); - }); - } -} - -class _MobileToolbar extends StatefulWidget { - const _MobileToolbar({ - required this.editorState, - required this.toolbarItems, - required this.toolbarHeight, - }); - - final EditorState editorState; - final List toolbarItems; - final double toolbarHeight; - - @override - State<_MobileToolbar> createState() => _MobileToolbarState(); -} - -class _MobileToolbarState extends State<_MobileToolbar> - implements AppFlowyMobileToolbarWidgetService { - // used to control the toolbar menu items - @override - PropertyValueNotifier showMenuNotifier = PropertyValueNotifier(false); - - // when the users click the menu item, the keyboard will be hidden, - // 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); - - // used to check if click the same item again - int? selectedMenuIndex; - - Selection? currentSelection; - - bool closeKeyboardInitiative = false; - - final ScrollOffsetListener offsetListener = ScrollOffsetListener.create(); - late final StreamSubscription offsetSubscription; - ValueNotifier toolbarOffset = ValueNotifier(0.0); - - @override - void initState() { - super.initState(); - - currentSelection = widget.editorState.selection; - KeyboardHeightObserver.instance.addListener(_onKeyboardHeightChanged); - offsetSubscription = offsetListener.changes.listen((event) { - toolbarOffset.value += event; - }); - } - - @override - void didUpdateWidget(covariant _MobileToolbar oldWidget) { - super.didUpdateWidget(oldWidget); - - if (currentSelection != widget.editorState.selection) { - currentSelection = widget.editorState.selection; - closeItemMenu(); - if (currentSelection != null) { - _showKeyboard(); - } - } - } - - @override - void dispose() { - showMenuNotifier.dispose(); - cachedKeyboardHeight.dispose(); - KeyboardHeightObserver.instance.removeListener(_onKeyboardHeightChanged); - offsetSubscription.cancel(); - toolbarOffset.dispose(); - - super.dispose(); - } - - @override - void reassemble() { - super.reassemble(); - - canUpdateCachedKeyboardHeight = true; - closeItemMenu(); - _closeKeyboard(); - } - - @override - Widget build(BuildContext context) { - // toolbar - // - if the menu is shown, the toolbar will be pushed up by the height of the menu - // - otherwise, add a spacer to push the toolbar up when the keyboard is shown - return Column( - children: [ - const Divider( - height: 0.5, - color: Color(0x7FEDEDED), - ), - _buildToolbar(context), - const Divider( - height: 0.5, - color: Color(0x7FEDEDED), - ), - _buildMenuOrSpacer(context), - ], - ); - } - - @override - void closeItemMenu() { - showMenuNotifier.value = false; - } - - @override - void closeKeyboard() { - _closeKeyboard(); - } - - void showItemMenu() { - showMenuNotifier.value = true; - } - - void _onKeyboardHeightChanged(double height) { - // if the keyboard is not closed initiative, we need to close the menu at same time - if (!closeKeyboardInitiative && - cachedKeyboardHeight.value != 0 && - height == 0) { - if (!widget.editorState.isDisposed) { - widget.editorState.selection = null; - } - } - - // if the menu is shown and the height is not 0, we need to close the menu - if (showMenuNotifier.value && height != 0) { - closeItemMenu(); - } - - 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) { - closeKeyboardInitiative = false; - } - } - - // toolbar list view and close keyboard/menu button - Widget _buildToolbar(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - return Container( - height: widget.toolbarHeight, - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - color: theme.toolbarBackgroundColor, - boxShadow: const [ - BoxShadow( - color: Color(0x0F181818), - blurRadius: 40, - offset: Offset(0, -4), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // toolbar list view - Expanded( - child: _ToolbarItemListView( - offsetListener: offsetListener, - toolbarItems: widget.toolbarItems, - editorState: widget.editorState, - toolbarWidgetService: this, - itemWithActionOnPressed: (_) { - if (showMenuNotifier.value) { - closeItemMenu(); - _showKeyboard(); - // update the cached keyboard height after the keyboard is shown - Debounce.debounce('canUpdateCachedKeyboardHeight', - const Duration(milliseconds: 500), () { - canUpdateCachedKeyboardHeight = true; - }); - } - }, - itemWithMenuOnPressed: (index) { - // click the same one - if (selectedMenuIndex == index && showMenuNotifier.value) { - // if the menu is shown, close it and show the keyboard - closeItemMenu(); - _showKeyboard(); - // update the cached keyboard height after the keyboard is shown - Debounce.debounce('canUpdateCachedKeyboardHeight', - const Duration(milliseconds: 500), () { - canUpdateCachedKeyboardHeight = true; - }); - } else { - canUpdateCachedKeyboardHeight = false; - selectedMenuIndex = index; - showItemMenu(); - closeKeyboardInitiative = true; - _closeKeyboard(); - } - }, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 13.0), - child: VerticalDivider( - width: 1.0, - thickness: 1.0, - color: Color(0xFFD9D9D9), - ), - ), - // close menu or close keyboard button - CloseKeyboardOrMenuButton( - onPressed: () { - closeKeyboardInitiative = true; - // close the keyboard and clear the selection - // if the selection is null, the keyboard and the toolbar will be hidden automatically - widget.editorState.selection = null; - - // sometimes, the keyboard is not closed after the selection is cleared - if (Platform.isAndroid) { - SystemChannels.textInput.invokeMethod('TextInput.hide'); - } - }, - ), - const HSpace(4.0), - ], - ), - ); - } - - // if there's no menu, we need to add a spacer to push the toolbar up when the keyboard is shown - Widget _buildMenuOrSpacer(BuildContext context) { - return ValueListenableBuilder( - valueListenable: cachedKeyboardHeight, - builder: (_, height, ___) { - 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; - } - return SizedBox( - height: keyboardHeight, - child: (showingMenu && selectedMenuIndex != null) - ? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call( - context, - widget.editorState, - this, - ) ?? - const SizedBox.shrink() - : const SizedBox.shrink(), - ); - }, - ); - }, - ); - } - - void _showKeyboard() { - final selection = widget.editorState.selection; - if (selection != null) { - widget.editorState.service.keyboardService?.enableKeyBoard(selection); - } - } - - void _closeKeyboard() { - widget.editorState.service.keyboardService?.closeKeyboard(); - } -} - -class _ToolbarItemListView extends StatefulWidget { - const _ToolbarItemListView({ - required this.offsetListener, - required this.toolbarItems, - required this.editorState, - required this.toolbarWidgetService, - required this.itemWithMenuOnPressed, - required this.itemWithActionOnPressed, - }); - - final Function(int index) itemWithMenuOnPressed; - final Function(int index) itemWithActionOnPressed; - final List toolbarItems; - final EditorState editorState; - final AppFlowyMobileToolbarWidgetService toolbarWidgetService; - final ScrollOffsetListener offsetListener; - - @override - State<_ToolbarItemListView> createState() => _ToolbarItemListViewState(); -} - -class _ToolbarItemListViewState extends State<_ToolbarItemListView> { - final scrollController = ItemScrollController(); - Selection? previousSelection; - - @override - void initState() { - super.initState(); - - widget.editorState.selectionNotifier - .addListener(_debounceUpdatePilotPosition); - previousSelection = widget.editorState.selection; - } - - @override - void dispose() { - widget.editorState.selectionNotifier - .removeListener(_debounceUpdatePilotPosition); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - const left = 8.0; - const right = 4.0; - // 68.0 is the width of the close keyboard/menu button - final padding = _calculatePadding(left + right + 68.0); - - final children = [ - const HSpace(left), - ...widget.toolbarItems - .mapIndexed( - (index, element) => element.itemBuilder.call( - context, - widget.editorState, - widget.toolbarWidgetService, - element.menuBuilder != null - ? () { - widget.itemWithMenuOnPressed(index); - } - : null, - element.menuBuilder == null - ? () { - widget.itemWithActionOnPressed(index); - } - : null, - ), - ) - .map((e) => [e, HSpace(padding)]) - .flattened, - const HSpace(right), - ]; - - return PageStorage( - bucket: PageStorageBucket(), - child: ScrollablePositionedList.builder( - physics: const ClampingScrollPhysics(), - scrollOffsetListener: widget.offsetListener, - itemScrollController: scrollController, - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) => children[index], - itemCount: children.length, - ), - ); - } - - double _calculatePadding(double extent) { - final screenWidth = MediaQuery.of(context).size.width; - final width = screenWidth - extent; - final int count; - if (screenWidth <= 340) { - count = 5; - } else if (screenWidth <= 384) { - count = 6; - } else if (screenWidth <= 430) { - count = 7; - } else { - count = 8; - } - // left + item count * width + item count * padding + right + close button width = screenWidth - return (width - count * 40.0) / count; - } - - void _debounceUpdatePilotPosition() { - Debounce.debounce( - 'updatePilotPosition', - const Duration(milliseconds: 250), - _updatePilotPosition, - ); - } - - void _updatePilotPosition() { - final selection = widget.editorState.selection; - if (selection == null) { - return; - } - - if (previousSelection != null && - previousSelection!.isCollapsed == selection.isCollapsed) { - return; - } - - final toolbarItems = widget.toolbarItems; - // use -0.4 to make sure the pilot is in the front of the toolbar item - final alignment = selection.isCollapsed ? 0.0 : -0.4; - final index = toolbarItems.indexWhere( - (element) => selection.isCollapsed - ? element.pilotAtCollapsedSelection - : element.pilotAtExpandedSelection, - ); - if (index != -1) { - scrollController.scrollTo( - alignment: alignment, - index: index, - duration: const Duration( - milliseconds: 250, - ), - ); - } - - previousSelection = selection; - } -} 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 deleted file mode 100644 index 12e7d1bef7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart +++ /dev/null @@ -1,147 +0,0 @@ -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'; -import 'package:flutter/material.dart'; - -// build the toolbar item, like Aa, +, image ... -typedef AppFlowyMobileToolbarItemBuilder = Widget Function( - BuildContext context, - EditorState editorState, - AppFlowyMobileToolbarWidgetService service, - VoidCallback? onMenuCallback, - VoidCallback? onActionCallback, -); - -// build the menu after clicking the toolbar item -typedef AppFlowyMobileToolbarItemMenuBuilder = Widget Function( - BuildContext context, - EditorState editorState, - AppFlowyMobileToolbarWidgetService service, -); - -class AppFlowyMobileToolbarItem { - /// Tool bar item that implements attribute directly(without opening menu) - const AppFlowyMobileToolbarItem({ - required this.itemBuilder, - this.menuBuilder, - this.pilotAtCollapsedSelection = false, - this.pilotAtExpandedSelection = false, - }); - - final AppFlowyMobileToolbarItemBuilder itemBuilder; - final AppFlowyMobileToolbarItemMenuBuilder? menuBuilder; - final bool pilotAtCollapsedSelection; - final bool pilotAtExpandedSelection; -} - -class AppFlowyMobileToolbarIconItem extends StatefulWidget { - const AppFlowyMobileToolbarIconItem({ - super.key, - this.icon, - this.keepSelectedStatus = false, - this.iconBuilder, - this.isSelected, - this.shouldListenToToggledStyle = false, - this.enable, - required this.onTap, - required this.editorState, - }); - - final FlowySvgData? icon; - final bool keepSelectedStatus; - final VoidCallback onTap; - final WidgetBuilder? iconBuilder; - final bool Function()? isSelected; - final bool shouldListenToToggledStyle; - final EditorState editorState; - final bool Function()? enable; - - @override - State createState() => - _AppFlowyMobileToolbarIconItemState(); -} - -class _AppFlowyMobileToolbarIconItemState - extends State { - bool isSelected = false; - StreamSubscription? _subscription; - - @override - void initState() { - super.initState(); - - isSelected = widget.isSelected?.call() ?? false; - if (widget.shouldListenToToggledStyle) { - widget.editorState.toggledStyleNotifier.addListener(_rebuild); - _subscription = widget.editorState.transactionStream.listen((_) { - _rebuild(); - }); - } - } - - @override - void dispose() { - if (widget.shouldListenToToggledStyle) { - widget.editorState.toggledStyleNotifier.removeListener(_rebuild); - _subscription?.cancel(); - } - super.dispose(); - } - - @override - void didUpdateWidget(covariant AppFlowyMobileToolbarIconItem oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.isSelected != null) { - isSelected = widget.isSelected!.call(); - } - } - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - final enable = widget.enable?.call() ?? true; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: AnimatedGestureDetector( - scaleFactor: 0.95, - onTapUp: () { - widget.onTap(); - _rebuild(); - }, - child: widget.iconBuilder?.call(context) ?? - Container( - width: 40, - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9), - color: isSelected - ? theme.toolbarItemSelectedBackgroundColor - : null, - ), - child: FlowySvg( - widget.icon!, - color: enable - ? theme.toolbarItemIconColor - : theme.toolbarItemIconDisabledColor, - ), - ), - ), - ); - } - - void _rebuild() { - if (!mounted) { - return; - } - setState(() { - isSelected = (widget.keepSelectedStatus && widget.isSelected == null) - ? !isSelected - : widget.isSelected?.call() ?? false; - }); - } -} 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/basic_toolbar_item.dart deleted file mode 100644 index 518060ccf5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/basic_toolbar_item.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.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/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/widgets.dart'; - -final boldToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - isSelected: () => - editorState.isTextDecorationSelected( - AppFlowyRichTextKeys.bold, - ) && - editorState.toggledStyle[AppFlowyRichTextKeys.bold] != false, - icon: FlowySvgs.m_toolbar_bold_m, - onTap: () async => editorState.toggleAttribute( - AppFlowyRichTextKeys.bold, - selectionExtraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - }, - ), - ); - }, -); - -final italicToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - isSelected: () => editorState.isTextDecorationSelected( - AppFlowyRichTextKeys.italic, - ), - icon: FlowySvgs.m_toolbar_italic_m, - onTap: () async => editorState.toggleAttribute( - AppFlowyRichTextKeys.italic, - selectionExtraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - }, - ), - ); - }, -); - -final underlineToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - isSelected: () => editorState.isTextDecorationSelected( - AppFlowyRichTextKeys.underline, - ), - icon: FlowySvgs.m_toolbar_underline_m, - onTap: () async => editorState.toggleAttribute( - AppFlowyRichTextKeys.underline, - selectionExtraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - }, - ), - ); - }, -); - -final strikethroughToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - isSelected: () => editorState.isTextDecorationSelected( - AppFlowyRichTextKeys.strikethrough, - ), - icon: FlowySvgs.m_toolbar_strike_m, - onTap: () async => editorState.toggleAttribute( - AppFlowyRichTextKeys.strikethrough, - selectionExtraInfo: { - selectionExtraInfoDisableFloatingToolbar: true, - }, - ), - ); - }, -); - -final colorToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, service, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - icon: FlowySvgs.m_aa_font_color_m, - iconBuilder: (context) { - String? getColor(String key) { - final selection = editorState.selection; - if (selection == null) { - return null; - } - String? color = editorState.toggledStyle[key]; - if (color == null) { - if (selection.isCollapsed && selection.startIndex != 0) { - color = editorState.getDeltaAttributeValueInSelection( - key, - selection.copyWith( - start: selection.start.copyWith( - offset: selection.startIndex - 1, - ), - ), - ); - } else { - color = editorState.getDeltaAttributeValueInSelection( - key, - ); - } - } - return color; - } - - final textColor = getColor(AppFlowyRichTextKeys.textColor); - final backgroundColor = getColor(AppFlowyRichTextKeys.backgroundColor); - - return Container( - width: 40, - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9), - color: EditorFontColors.fromBuiltInColors( - context, - backgroundColor?.tryToColor(), - ), - ), - child: FlowySvg( - FlowySvgs.m_aa_font_color_m, - color: EditorFontColors.fromBuiltInColors( - context, - textColor?.tryToColor(), - ), - ), - ); - }, - onTap: () { - service.closeKeyboard(); - editorState.updateSelectionWithReason( - editorState.selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ); - keepEditorFocusNotifier.increase(); - showTextColorAndBackgroundColorPicker( - context, - editorState: editorState, - selection: editorState.selection!, - ); - }, - ); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/indent_outdent_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/indent_outdent_toolbar_item.dart deleted file mode 100644 index 290fa2d3e0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/indent_outdent_toolbar_item.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final indentToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - keepSelectedStatus: true, - isSelected: () => false, - enable: () => isIndentable(editorState), - icon: FlowySvgs.m_aa_indent_m, - onTap: () async { - indentCommand.execute(editorState); - }, - ); - }, -); - -final outdentToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - keepSelectedStatus: true, - isSelected: () => false, - enable: () => isOutdentable(editorState), - icon: FlowySvgs.m_aa_outdent_m, - onTap: () async { - outdentCommand.execute(editorState); - }, - ); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart deleted file mode 100644 index d72f722eb6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:io'; - -import 'package:keyboard_height_plugin/keyboard_height_plugin.dart'; - -typedef KeyboardHeightCallback = void Function(double height); - -// the KeyboardHeightPlugin only accepts one listener, so we need to create a -// singleton class to manage the multiple listeners. -class KeyboardHeightObserver { - KeyboardHeightObserver._() { - _keyboardHeightPlugin.onKeyboardHeightChanged((height) { - notify(height); - }); - } - - static final KeyboardHeightObserver instance = KeyboardHeightObserver._(); - static double currentKeyboardHeight = 0; - - final List _listeners = []; - final KeyboardHeightPlugin _keyboardHeightPlugin = KeyboardHeightPlugin(); - - void addListener(KeyboardHeightCallback listener) { - _listeners.add(listener); - } - - void removeListener(KeyboardHeightCallback listener) { - _listeners.remove(listener); - } - - void dispose() { - _listeners.clear(); - _keyboardHeightPlugin.dispose(); - } - - void notify(double height) { - // the keyboard height will notify twice with the same value on Android - if (Platform.isAndroid && height == currentKeyboardHeight) { - return; - } - for (final listener in _listeners) { - listener(height); - } - currentKeyboardHeight = height; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/list_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/list_toolbar_item.dart deleted file mode 100644 index 240ea7072e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/list_toolbar_item.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final todoListToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - keepSelectedStatus: true, - isSelected: () => false, - icon: FlowySvgs.m_toolbar_checkbox_m, - onTap: () async { - await editorState.convertBlockType( - TodoListBlockKeys.type, - extraAttributes: { - TodoListBlockKeys.checked: false, - }, - ); - }, - ); - }, -); - -final numberedListToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - final isSelected = - editorState.isBlockTypeSelected(NumberedListBlockKeys.type); - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - keepSelectedStatus: true, - isSelected: () => isSelected, - icon: FlowySvgs.m_toolbar_numbered_list_m, - onTap: () async { - await editorState.convertBlockType( - NumberedListBlockKeys.type, - ); - }, - ); - }, -); - -final bulletedListToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - final isSelected = - editorState.isBlockTypeSelected(BulletedListBlockKeys.type); - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - keepSelectedStatus: true, - isSelected: () => isSelected, - icon: FlowySvgs.m_toolbar_bulleted_list_m, - onTap: () async { - await editorState.convertBlockType( - BulletedListBlockKeys.type, - ); - }, - ); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/more_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/more_toolbar_item.dart deleted file mode 100644 index 1488847ea5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/more_toolbar_item.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; - -final moreToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - icon: FlowySvgs.m_toolbar_more_s, - onTap: () {}, - ); - }, -); 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 deleted file mode 100644 index 35a3c37e74..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final _listBlockTypes = [ - BulletedListBlockKeys.type, - NumberedListBlockKeys.type, - TodoListBlockKeys.type, -]; - -final _defaultToolbarItems = [ - addBlockToolbarItem, - aaToolbarItem, - todoListToolbarItem, - bulletedListToolbarItem, - addAttachmentItem, - numberedListToolbarItem, - boldToolbarItem, - italicToolbarItem, - underlineToolbarItem, - strikethroughToolbarItem, - colorToolbarItem, - undoToolbarItem, - redoToolbarItem, -]; - -final _listToolbarItems = [ - addBlockToolbarItem, - aaToolbarItem, - outdentToolbarItem, - indentToolbarItem, - todoListToolbarItem, - bulletedListToolbarItem, - numberedListToolbarItem, - boldToolbarItem, - italicToolbarItem, - underlineToolbarItem, - strikethroughToolbarItem, - colorToolbarItem, - addAttachmentItem, - undoToolbarItem, - redoToolbarItem, -]; - -final _textToolbarItems = [ - aaToolbarItem, - boldToolbarItem, - italicToolbarItem, - underlineToolbarItem, - strikethroughToolbarItem, - colorToolbarItem, -]; - -/// Calculate the toolbar items based on the current selection. -/// -/// Default: -/// Add, Aa, Todo List, Image, Bulleted List, Numbered List, B, I, U, S, Color, Undo, Redo -/// -/// Selecting text: -/// Aa, B, I, U, S, Color -/// -/// Selecting a list: -/// Add, Aa, Indent, Outdent, Bulleted List, Numbered List, Todo List B, I, U, S -List buildMobileToolbarItems( - EditorState editorState, - Selection? selection, -) { - if (selection == null) { - return []; - } - - if (!selection.isCollapsed) { - return _textToolbarItems; - } - - final allSelectedAreListType = editorState - .getSelectedNodes(selection: selection) - .every((node) => _listBlockTypes.contains(node.type)); - if (allSelectedAreListType) { - return _listToolbarItems; - } - - return _defaultToolbarItems; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart deleted file mode 100644 index 5578d8a33c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart +++ /dev/null @@ -1,57 +0,0 @@ -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/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/widgets.dart'; - -final undoToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - final theme = ToolbarColorExtension.of(context); - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - iconBuilder: (context) { - final canUndo = editorState.undoManager.undoStack.isNonEmpty; - return Container( - width: 40, - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9), - ), - child: FlowySvg( - FlowySvgs.m_toolbar_undo_m, - color: canUndo - ? theme.toolbarItemIconColor - : theme.toolbarItemIconDisabledColor, - ), - ); - }, - onTap: () => undoCommand.execute(editorState), - ); - }, -); - -final redoToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - final theme = ToolbarColorExtension.of(context); - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - iconBuilder: (context) { - final canRedo = editorState.undoManager.redoStack.isNonEmpty; - return Container( - width: 40, - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9), - ), - child: FlowySvg( - FlowySvgs.m_toolbar_redo_m, - color: canRedo - ? theme.toolbarItemIconColor - : theme.toolbarItemIconDisabledColor, - ), - ); - }, - onTap: () => redoCommand.execute(editorState), - ); - }, -); 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 deleted file mode 100644 index 37444cd6e1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart +++ /dev/null @@ -1,358 +0,0 @@ -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({ - super.key, - required this.size, - this.icon, - this.text, - this.backgroundColor, - this.selectedBackgroundColor, - this.enable, - this.fontFamily, - required this.isSelected, - required this.iconPadding, - this.enableBottomLeftRadius = true, - this.enableBottomRightRadius = true, - this.enableTopLeftRadius = true, - this.enableTopRightRadius = true, - this.showDownArrow = false, - this.showRightArrow = false, - this.textPadding = EdgeInsets.zero, - required this.onTap, - this.iconColor, - }); - - final Size size; - final VoidCallback onTap; - final FlowySvgData? icon; - final String? text; - final bool? enable; - final String? fontFamily; - final bool isSelected; - final EdgeInsets iconPadding; - final bool enableTopLeftRadius; - final bool enableTopRightRadius; - final bool enableBottomRightRadius; - final bool enableBottomLeftRadius; - final bool showDownArrow; - final bool showRightArrow; - final Color? backgroundColor; - final Color? selectedBackgroundColor; - final EdgeInsets textPadding; - final Color? iconColor; - - @override - Widget build(BuildContext context) { - final theme = ToolbarColorExtension.of(context); - Color? iconColor = this.iconColor; - if (iconColor == null) { - if (enable != null) { - iconColor = enable! ? null : theme.toolbarMenuIconDisabledColor; - } else { - iconColor = isSelected - ? theme.toolbarMenuIconSelectedColor - : theme.toolbarMenuIconColor; - } - } - final textColor = - enable == false ? theme.toolbarMenuIconDisabledColor : null; - // the ui design is based on 375.0 width - final scale = context.scale; - final radius = Radius.circular(12 * scale); - final Widget child; - if (icon != null) { - child = FlowySvg(icon!, color: iconColor); - } else if (text != null) { - child = Padding( - padding: textPadding * scale, - child: FlowyText( - text!, - fontSize: 16.0, - color: textColor, - fontFamily: fontFamily, - overflow: TextOverflow.ellipsis, - ), - ); - } else { - throw ArgumentError('icon and text cannot be null at the same time'); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: enable == false ? null : onTap, - child: Stack( - children: [ - Container( - height: size.height * scale, - width: size.width * scale, - alignment: text != null ? Alignment.centerLeft : Alignment.center, - decoration: BoxDecoration( - color: isSelected - ? (selectedBackgroundColor ?? - theme.toolbarMenuItemSelectedBackgroundColor) - : backgroundColor, - borderRadius: BorderRadius.only( - topLeft: enableTopLeftRadius ? radius : Radius.zero, - topRight: enableTopRightRadius ? radius : Radius.zero, - bottomRight: enableBottomRightRadius ? radius : Radius.zero, - bottomLeft: enableBottomLeftRadius ? radius : Radius.zero, - ), - ), - padding: iconPadding * scale, - child: child, - ), - if (showDownArrow) - Positioned( - right: 9.0 * scale, - bottom: 9.0 * scale, - child: const FlowySvg(FlowySvgs.m_aa_down_arrow_s), - ), - if (showRightArrow) - Positioned.fill( - right: 12.0 * scale, - child: Align( - alignment: Alignment.centerRight, - child: FlowySvg( - FlowySvgs.m_aa_arrow_right_s, - color: iconColor, - ), - ), - ), - ], - ), - ); - } -} - -class ScaledVerticalDivider extends StatelessWidget { - const ScaledVerticalDivider({super.key}); - - @override - Widget build(BuildContext context) { - return HSpace(1.5 * context.scale); - } -} - -class ScaledVSpace extends StatelessWidget { - const ScaledVSpace({super.key}); - - @override - Widget build(BuildContext context) { - return VSpace(12.0 * context.scale); - } -} - -extension MobileToolbarBuildContext on BuildContext { - double get scale => MediaQuery.of(this).size.width / 375.0; -} - -final _blocksCanContainChildren = [ - ParagraphBlockKeys.type, - BulletedListBlockKeys.type, - NumberedListBlockKeys.type, - TodoListBlockKeys.type, -]; - -extension MobileToolbarEditorState on EditorState { - bool isBlockTypeSelected(String blockType, {int? level}) { - final selection = this.selection; - if (selection == null) { - return false; - } - final node = getNodeAtPath(selection.start.path); - final type = node?.type; - if (node == null || type == null) { - return false; - } - if (level != null && blockType == HeadingBlockKeys.type) { - return type == blockType && - node.attributes[HeadingBlockKeys.level] == level; - } - return type == blockType; - } - - bool isTextDecorationSelected(String richTextKey) { - final selection = this.selection; - if (selection == null) { - return false; - } - - final nodes = getNodesInSelection(selection); - bool isSelected = false; - if (selection.isCollapsed) { - if (toggledStyle.containsKey(richTextKey)) { - isSelected = toggledStyle[richTextKey] as bool; - } else { - if (selection.startIndex != 0) { - // get previous index text style - isSelected = nodes.allSatisfyInSelection( - selection.copyWith( - start: selection.start.copyWith( - offset: selection.startIndex - 1, - ), - ), - (delta) => delta.everyAttributes( - (attributes) => attributes[richTextKey] == true, - ), - ); - } - } - } else { - isSelected = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes((attr) => attr[richTextKey] == true); - }); - } - return isSelected; - } - - Future convertBlockType( - String newBlockType, { - Selection? selection, - Attributes? extraAttributes, - bool? isSelected, - Map? selectionExtraInfo, - }) async { - selection = selection ?? this.selection; - if (selection == null) { - return; - } - final node = getNodeAtPath(selection.start.path); - final type = node?.type; - if (node == null || type == null) { - assert(false, 'node or type is null'); - return; - } - final selected = isSelected ?? type == newBlockType; - - // if the new block type can't contain children, we need to move all the children to the parent - bool needToDeleteChildren = false; - if (!selected && - node.children.isNotEmpty && - !_blocksCanContainChildren.contains(newBlockType)) { - final transaction = this.transaction; - needToDeleteChildren = true; - transaction.insertNodes( - selection.end.path.next, - node.children.map((e) => e.deepCopy()), - ); - await apply(transaction); - } - await formatNode( - selection, - (node) { - final attributes = { - ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(), - // for some block types, they have extra attributes, like todo list has checked attribute, callout has icon attribute, etc. - if (!selected && extraAttributes != null) ...extraAttributes, - }; - return node.copyWith( - type: selected ? ParagraphBlockKeys.type : newBlockType, - attributes: attributes, - children: needToDeleteChildren ? [] : null, - ); - }, - selectionExtraInfo: selectionExtraInfo, - ); - } - - Future alignBlock( - String alignment, { - Selection? selection, - Map? selectionExtraInfo, - }) async { - await updateNode( - selection, - (node) => node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: alignment, - }, - ), - selectionExtraInfo: selectionExtraInfo, - ); - } - - Future updateTextAndHref( - String? prevText, - String? prevHref, - String? text, - String? href, { - Selection? selection, - }) async { - if (prevText == null && text == null) { - return; - } - - selection ??= this.selection; - // doesn't support multiple selection now - if (selection == null || !selection.isSingle) { - return; - } - - final node = getNodeAtPath(selection.start.path); - if (node == null) { - return; - } - - final transaction = this.transaction; - - // insert a new link - if (prevText == null && - text != null && - text.isNotEmpty && - selection.isCollapsed) { - final attributes = href != null && href.isNotEmpty - ? {AppFlowyRichTextKeys.href: href} - : null; - transaction.insertText( - node, - selection.startIndex, - text, - attributes: attributes, - ); - } else if (text != null && prevText != text) { - // update text - transaction.replaceText( - node, - selection.startIndex, - selection.length, - text, - ); - } - - // if the text is empty, it means the user wants to remove the text - if (text != null && text.isNotEmpty && prevHref != href) { - // update href - transaction.formatText( - node, - selection.startIndex, - text.length, - {AppFlowyRichTextKeys.href: href?.isEmpty == true ? null : href}, - ); - } - - 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 deleted file mode 100644 index f77083d21d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart +++ /dev/null @@ -1,142 +0,0 @@ -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'; -import 'package:numerus/roman/roman.dart'; - -class NumberedListIcon extends StatelessWidget { - const NumberedListIcon({ - 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, - ), - textDirection: textDirection, - ), - ); - } -} - -extension NumberedListNodeIndex on Node { - String buildLevelString(BuildContext context) { - final builder = NumberedListIndexBuilder( - editorState: context.read(), - node: this, - ); - final indexInRootLevel = builder.indexInRootLevel; - final indexInSameLevel = builder.indexInSameLevel; - final level = indexInRootLevel % 3; - final levelString = switch (level) { - 1 => indexInSameLevel.latin, - 2 => indexInSameLevel.roman, - _ => '$indexInSameLevel', - }; - return '$levelString.'; - } -} - -class NumberedListIndexBuilder { - NumberedListIndexBuilder({ - required this.editorState, - required this.node, - }); - - final EditorState editorState; - final Node node; - - // the level of the current node - int get indexInRootLevel { - var level = 0; - var parent = node.parent; - while (parent != null) { - if (parent.type == NumberedListBlockKeys.type) { - level++; - } - parent = parent.parent; - } - return level; - } - - // the index of the current level - int get indexInSameLevel { - int level = 1; - 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)) { - return node.attributes[NumberedListBlockKeys.number] ?? level; - } - - int? startNumber; - while (previous != null && previous.type == NumberedListBlockKeys.type) { - 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 level; - } -} - -extension on int { - String get latin { - String result = ''; - int number = this; - while (number > 0) { - final int remainder = (number - 1) % 26; - result = String.fromCharCode(remainder + 65) + result; - number = (number - 1) ~/ 26; - } - return result.toLowerCase(); - } - - String get roman { - return toRomanNumeralString() ?? '$this'; - } -} 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..124c11206a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart @@ -0,0 +1,231 @@ +import 'dart:convert'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart'; + +import 'text_completion.dart'; +import 'package:dartz/dartz.dart'; +import 'dart:async'; + +import 'error.dart'; +import 'package:http/http.dart' as http; + +// Please fill in your own API key +const apiKey = ''; + +enum OpenAIRequestType { + textCompletion, + textEdit; + + 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/edits'); + } + } +} + +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, + }); +} + +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': 'text-davinci-003', + '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 Right( + TextCompletionResponse.fromJson( + json.decode( + utf8.decode(response.bodyBytes), + ), + ), + ); + } else { + return Left(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': 'text-davinci-003', + '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': 'text-davinci-edit-001', + '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 Right( + TextEditResponse.fromJson( + json.decode( + utf8.decode(response.bodyBytes), + ), + ), + ); + } else { + return Left(OpenAIError.fromJson(json.decode(response.body)['error'])); + } + } +} 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..49b047c758 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart @@ -0,0 +1,10 @@ +import 'package:url_launcher/url_launcher.dart'; + +Future openLearnMorePage() async { + final uri = Uri.parse( + 'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai', + ); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } +} 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..eb36767e80 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart @@ -0,0 +1,550 @@ +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_editor/appflowy_editor.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:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +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( + name: 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(); + + 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: 3, + focusNode: textFieldFocusNode, + autoFocus: false, + ); + } + + Future _onExit() async { + final transaction = editorState.transaction..deleteNode(widget.node); + await editorState.apply( + transaction, + options: const ApplyOptions( + // disable undo/redo + recordRedo: false, + recordUndo: false, + ), + ); + } + + Future _onGenerate() async { + final loading = Loading(context); + loading.start(); + + await _updateEditingText(); + + final userProfile = await UserBackendService.getCurrentUserProfile() + .then((value) => value.toOption().toNullable()); + if (userProfile == null) { + loading.stop(); + await _showError( + LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(), + ); + 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 { + loading.stop(); + barrierDialog = BarrierDialog(context); + barrierDialog?.show(); + await _makeSurePreviousNodeIsEmptyParagraphNode(); + }, + onProcess: (response) async { + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + await textRobot.autoInsertText( + text, + inputType: TextRobotInputType.word, + delay: Duration.zero, + ); + } + }, + onEnd: () async { + await barrierDialog?.dismiss(); + }, + onError: (error) async { + loading.stop(); + await _showError(error.message); + }, + ); + 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(); + } + } + _onExit(); + } + + Future _onRewrite() async { + final previousOutput = _getPreviousOutput(); + if (previousOutput == null) { + return; + } + + final loading = Loading(context); + 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.toOption().toNullable()); + if (userProfile == null) { + loading.stop(); + await _showError( + LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(), + ); + return; + } + final textRobot = TextRobot(editorState: editorState); + final openAIRepository = HttpOpenAIRepository( + client: http.Client(), + apiKey: userProfile.openaiKey, + ); + await openAIRepository.getStreamedCompletions( + prompt: _rewritePrompt(previousOutput), + onStart: () async { + loading.stop(); + await _makeSurePreviousNodeIsEmptyParagraphNode(); + }, + onProcess: (response) async { + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + await textRobot.autoInsertText( + text, + inputType: TextRobotInputType.word, + delay: Duration.zero, + ); + } + }, + onEnd: () async {}, + onError: (error) async { + loading.stop(); + await _showError(error.message); + }, + ); + 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); + } + + Future _showError(String message) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + action: SnackBarAction( + label: LocaleKeys.button_Cancel.tr(), + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + content: FlowyText(message), + ), + ); + } + + 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..b2f314c425 --- /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..820c2db84c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class Loading { + Loading(this.context); + + late BuildContext loadingContext; + final BuildContext context; + + Future start() async => await 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 => Navigator.of(loadingContext).pop(); +} + +class BarrierDialog { + BarrierDialog(this.context); + + late BuildContext loadingContext; + final BuildContext context; + + Future show() async => await showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.transparent, + builder: (BuildContext context) { + loadingContext = context; + return Container(); + }, + ); + + 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..9c67de7493 --- /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 { + final SmartEditAction inner; + + SmartEditActionWrapper(this.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..dc1e6be586 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart @@ -0,0 +1,477 @@ +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_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 { + 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 && padding != 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 => await _onExit(), + ), + const Spacer(flex: 1), + 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, offset: 0), + 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( + recordRedo: false, + 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 { + await _showError(error.message); + await _onExit(); + }, + ); + } + } + + Future _showError(String message) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + action: SnackBarAction( + label: LocaleKeys.button_Cancel.tr(), + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + content: FlowyText(message), + ), + ); + } +} 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..2ba502859b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart @@ -0,0 +1,135 @@ +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/widgets/pop_up_action.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +final ToolbarItem smartEditItem = ToolbarItem( + id: 'appflowy.editor.smart_edit', + group: 0, + isActive: (editorState) { + final selection = editorState.selection; + if (selection == null) { + return false; + } + final nodes = editorState.getNodesInSelection(selection); + return nodes.every((element) => element.delta != null); + }, + 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( + (l) => false, + (r) => r.openaiKey.isNotEmpty, + ); + }); + }); + } + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithLeftAligned, + actions: SmartEditAction.values + .map((action) => SmartEditActionWrapper(action)) + .toList(), + buildChild: (controller) { + 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 { + _showError(LocaleKeys.document_plugins_smartEditDisabled.tr()); + } + }, + ); + }, + 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, + recordRedo: false, + ), + withUpdateSelection: false, + ); + } + + Future _showError(String message) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + action: SnackBarAction( + label: LocaleKeys.button_Cancel.tr(), + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + content: FlowyText(message), + ), + ); + } +} 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 deleted file mode 100644 index e3120356d9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ /dev/null @@ -1,346 +0,0 @@ -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:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class OutlineBlockKeys { - const OutlineBlockKeys._(); - - static const String type = 'outline'; - static const String backgroundColor = blockComponentBackgroundColor; - static const String depth = 'depth'; -} - -Node outlineBlockNode() { - return Node( - type: OutlineBlockKeys.type, - ); -} - -enum _OutlineBlockStatus { - noHeadings, - noMatchHeadings, - success; -} - -final _availableBlockTypes = [ - HeadingBlockKeys.type, - ToggleListBlockKeys.type, -]; - -class OutlineBlockComponentBuilder extends BlockComponentBuilder { - OutlineBlockComponentBuilder({ - super.configuration, - }); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return OutlineBlockWidget( - key: node.key, - node: node, - configuration: configuration, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isEmpty; -} - -class OutlineBlockWidget extends BlockComponentStatefulWidget { - const OutlineBlockWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => _OutlineBlockWidgetState(); -} - -class _OutlineBlockWidgetState extends State - with - BlockComponentConfigurable, - BlockComponentTextDirectionMixin, - BlockComponentBackgroundColorMixin, - DefaultSelectableMixin, - SelectableMixin { - // Change the value if the heading block type supports heading levels greater than '3' - static const maxVisibleDepth = 6; - - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - @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; - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: stream, - 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 (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, - ), - ); - } - - return child; - }, - ); - } - - Widget _buildOutlineBlock() { - final textDirection = calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ); - final (status, headings) = getHeadingNodes(); - - Widget child; - - switch (status) { - case _OutlineBlockStatus.noHeadings: - child = Align( - alignment: Alignment.centerLeft, - child: Text( - LocaleKeys.document_plugins_outline_addHeadingToCreateOutline.tr(), - style: configuration.placeholderTextStyle(node), - ), - ); - case _OutlineBlockStatus.noMatchHeadings: - child = Align( - alignment: Alignment.centerLeft, - child: Text( - LocaleKeys.document_plugins_outline_noMatchHeadings.tr(), - style: configuration.placeholderTextStyle(node), - ), - ); - case _OutlineBlockStatus.success: - final children = headings - .map( - (e) => Container( - padding: const EdgeInsets.only( - bottom: 4.0, - ), - width: double.infinity, - child: OutlineItemWidget( - node: e, - textDirection: textDirection, - ), - ), - ) - .toList(); - child = Padding( - padding: const EdgeInsets.only(left: 15.0), - child: Column( - children: children, - ), - ); - } - - return Container( - key: blockComponentKey, - constraints: const BoxConstraints( - minHeight: 40.0, - ), - padding: UniversalPlatform.isMobile ? EdgeInsets.zero : padding, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 2.0, - horizontal: 5.0, - ), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - color: backgroundColor, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - textDirection: textDirection, - children: [ - Text( - LocaleKeys.document_outlineBlock_placeholder.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const VSpace(8.0), - child, - ], - ), - ), - ); - } - - (_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), - ); - 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), - ); - 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 { - OutlineItemWidget({ - super.key, - required this.node, - required this.textDirection, - }) { - assert(_availableBlockTypes.contains(node.type)); - } - - final Node node; - final TextDirection textDirection; - - @override - Widget build(BuildContext context) { - 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, - ), - ), - ], - ), - ); - } - - void scrollToBlock(BuildContext context) { - final editorState = context.read(); - final editorScrollController = context.read(); - editorScrollController.itemScrollController.jumpTo( - index: node.path.first, - alignment: 0.5, - ); - editorState.selection = Selection.collapsed( - Position(path: node.path, offset: node.delta?.length ?? 0), - ); - } -} - -extension on Node { - double get leftIndent { - assert(_availableBlockTypes.contains(type)); - - if (!_availableBlockTypes.contains(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; - } - - String get outlineItemText { - return delta?.toPlainText() ?? ''; - } -} 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_cover_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart deleted file mode 100644 index 9b26daa3e3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart +++ /dev/null @@ -1,287 +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_plugins/page_style/_page_style_util.dart'; -import 'package:appflowy/shared/feedback_gesture_detector.dart'; -import 'package:appflowy/shared/flowy_gradient_colors.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 PageCoverBottomSheet extends StatelessWidget { - const PageCoverBottomSheet({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(8.0), - - // pure colors - FlowyText( - LocaleKeys.pageStyle_colors.tr(), - color: context.pageStyleTextColor, - fontSize: 14.0, - ), - const VSpace(8.0), - _buildPureColors(context, state), - const VSpace(20.0), - - // gradient colors - FlowyText( - LocaleKeys.pageStyle_gradient.tr(), - color: context.pageStyleTextColor, - fontSize: 14.0, - ), - const VSpace(8.0), - _buildGradientColors(context, state), - const VSpace(20.0), - - // built-in images - FlowyText( - LocaleKeys.pageStyle_backgroundImage.tr(), - color: context.pageStyleTextColor, - fontSize: 14.0, - ), - const VSpace(8.0), - _buildBuiltImages(context, state), - ], - ), - ); - }, - ); - } - - Widget _buildPureColors( - BuildContext context, - DocumentPageStyleState state, - ) { - return SizedBox( - height: 42.0, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: FlowyTint.values.length, - separatorBuilder: (context, index) => const HSpace(12.0), - itemBuilder: (context, index) => _buildColorButton( - context, - state, - FlowyTint.values[index], - ), - ), - ); - } - - Widget _buildGradientColors( - BuildContext context, - DocumentPageStyleState state, - ) { - return SizedBox( - height: 42.0, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: FlowyGradientColor.values.length, - separatorBuilder: (context, index) => const HSpace(12.0), - itemBuilder: (context, index) => _buildGradientButton( - context, - state, - FlowyGradientColor.values[index], - ), - ), - ); - } - - Widget _buildColorButton( - BuildContext context, - DocumentPageStyleState state, - FlowyTint tint, - ) { - final isSelected = - state.coverImage.isPureColor && state.coverImage.value == tint.id; - - final child = !isSelected - ? Container( - width: 42, - height: 42, - decoration: ShapeDecoration( - color: tint.color(context), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(21), - ), - ), - ) - : Container( - width: 42, - height: 42, - decoration: ShapeDecoration( - color: Colors.transparent, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1.50, - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(21), - ), - ), - alignment: Alignment.center, - child: Container( - width: 34, - height: 34, - decoration: ShapeDecoration( - color: tint.color(context), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(17), - ), - ), - ), - ); - - return FeedbackGestureDetector( - onTap: () { - context.read().add( - DocumentPageStyleEvent.updateCoverImage( - PageStyleCover( - type: PageStyleCoverImageType.pureColor, - value: tint.id, - ), - ), - ); - }, - child: child, - ); - } - - Widget _buildGradientButton( - BuildContext context, - DocumentPageStyleState state, - FlowyGradientColor gradientColor, - ) { - final isSelected = state.coverImage.isGradient && - state.coverImage.value == gradientColor.id; - - final child = !isSelected - ? Container( - width: 42, - height: 42, - decoration: ShapeDecoration( - gradient: gradientColor.linear, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(21), - ), - ), - ) - : Container( - width: 42, - height: 42, - decoration: ShapeDecoration( - color: Colors.transparent, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1.50, - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(21), - ), - ), - alignment: Alignment.center, - child: Container( - width: 34, - height: 34, - decoration: ShapeDecoration( - gradient: gradientColor.linear, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(17), - ), - ), - ), - ); - - return FeedbackGestureDetector( - onTap: () { - context.read().add( - DocumentPageStyleEvent.updateCoverImage( - PageStyleCover( - type: PageStyleCoverImageType.gradientColor, - value: gradientColor.id, - ), - ), - ); - }, - child: child, - ); - } - - Widget _buildBuiltImages( - BuildContext context, - DocumentPageStyleState state, - ) { - final imageNames = ['1', '2', '3', '4', '5', '6']; - return GridView.builder( - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 16.0 / 9.0, - ), - itemCount: imageNames.length, - itemBuilder: (context, index) => _buildBuiltInImage( - context, - state, - imageNames[index], - ), - ); - } - - Widget _buildBuiltInImage( - BuildContext context, - DocumentPageStyleState state, - String imageName, - ) { - final asset = PageStyleCoverImageType.builtInImagePath(imageName); - final isSelected = - state.coverImage.isBuiltInImage && state.coverImage.value == imageName; - final image = ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.asset( - asset, - fit: BoxFit.cover, - ), - ); - final child = !isSelected - ? image - : Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide(width: 1.50, color: Color(0xFF00BCF0)), - borderRadius: BorderRadius.circular(6), - ), - ), - padding: const EdgeInsets.all(2.0), - child: image, - ); - - return FeedbackGestureDetector( - onTap: () { - context.read().add( - DocumentPageStyleEvent.updateCoverImage( - PageStyleCover( - type: PageStyleCoverImageType.builtInImage, - value: imageName, - ), - ), - ); - }, - child: child, - ); - } -} 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 deleted file mode 100644 index 45a23bc6ac..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ /dev/null @@ -1,410 +0,0 @@ -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'; -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/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/feedback_gesture_detector.dart'; -import 'package:appflowy/shared/flowy_gradient_colors.dart'; -import 'package:appflowy/shared/permission/permission_checker.dart'; -import 'package:appflowy/user/application/user_service.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: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:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -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 - Widget build(BuildContext context) { - final backgroundColor = context.pageStyleBackgroundColor; - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - _buildOptionGroup(context, backgroundColor, state), - const VSpace(16.0), - _buildPreview(context, state), - ], - ); - }, - ); - } - - Widget _buildOptionGroup( - BuildContext context, - Color backgroundColor, - DocumentPageStyleState state, - ) { - return Container( - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: const BorderRadius.horizontal( - left: Radius.circular(12), - right: Radius.circular(12), - ), - ), - padding: const EdgeInsets.all(4.0), - child: Row( - children: [ - _CoverOptionButton( - showLeftCorner: true, - showRightCorner: false, - selected: state.coverImage.isPresets, - onTap: () => _showPresets(context), - child: const _PresetCover(), - ), - _CoverOptionButton( - showLeftCorner: false, - showRightCorner: false, - selected: state.coverImage.isPhoto, - onTap: () => _pickImage(context), - child: const _PhotoCover(), - ), - _CoverOptionButton( - showLeftCorner: false, - showRightCorner: true, - selected: state.coverImage.isUnsplashImage, - onTap: () => _showUnsplash(context), - child: const _UnsplashCover(), - ), - ], - ), - ); - } - - Widget _buildPreview( - BuildContext context, - DocumentPageStyleState state, - ) { - final cover = state.coverImage; - if (cover.isNone) { - return const SizedBox.shrink(); - } - - final value = cover.value; - final type = cover.type; - - Widget preview = const SizedBox.shrink(); - - if (type == PageStyleCoverImageType.customImage || - type == PageStyleCoverImageType.unsplashImage) { - final userProfilePB = - context.read().state.userProfilePB; - preview = FlowyNetworkImage( - url: value, - userProfilePB: userProfilePB, - ); - } - - if (type == PageStyleCoverImageType.builtInImage) { - preview = Image.asset( - PageStyleCoverImageType.builtInImagePath(value), - fit: BoxFit.cover, - ); - } - - if (type == PageStyleCoverImageType.pureColor) { - final color = value.coverColor(context); - if (color != null) { - preview = ColoredBox( - color: color, - ); - } - } - - if (type == PageStyleCoverImageType.gradientColor) { - preview = Container( - decoration: BoxDecoration( - gradient: FlowyGradientColor.fromId(value).linear, - ), - ); - } - - if (type == PageStyleCoverImageType.localImage) { - preview = Image.file( - File(value), - fit: BoxFit.cover, - ); - } - - return Row( - children: [ - FlowyText(LocaleKeys.pageStyle_image.tr()), - const Spacer(), - Container( - width: 40, - height: 28, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - border: Border.all(color: const Color(0x1F222533)), - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - child: preview, - ), - ), - ], - ); - } - - void _showPresets(BuildContext context) { - final pageStyleBloc = context.read(); - - context.pop(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - showDoneButton: true, - showHeader: true, - showRemoveButton: true, - onRemove: () { - pageStyleBloc.add( - DocumentPageStyleEvent.updateCoverImage( - PageStyleCover.none(), - ), - ); - }, - title: LocaleKeys.pageStyle_presets.tr(), - backgroundColor: AFThemeExtension.of(context).background, - builder: (_) { - return BlocProvider.value( - value: pageStyleBloc, - child: const PageCoverBottomSheet(), - ); - }, - ); - } - - Future _pickImage(BuildContext context) async { - final photoPermission = - await PermissionChecker.checkPhotoPermission(context); - if (!photoPermission) { - Log.error('Has no permission to access the photo library'); - return; - } - - XFile? result; - try { - result = await _imagePicker.pickImage(source: ImageSource.gallery); - } catch (e) { - Log.error('Error while picking image: $e'); - return; - } - - final path = result?.path; - if (path != null && context.mounted) { - final String? result; - final userProfile = await UserBackendService.getCurrentUserProfile().fold( - (s) => s, - (f) => null, - ); - final isAppFlowyCloud = - userProfile?.workspaceAuthType == AuthTypePB.Server; - 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); - type = PageStyleCoverImageType.customImage; - } - if (!context.mounted) { - return; - } - if (result == null) { - return showSnapBar( - context, - LocaleKeys.document_plugins_image_imageUploadFailed.tr(), - ); - } - - context.read().add( - DocumentPageStyleEvent.updateCoverImage( - PageStyleCover(type: type, value: result), - ), - ); - } - } - - void _showUnsplash(BuildContext context) { - final pageStyleBloc = context.read(); - final backgroundColor = AFThemeExtension.of(context).background; - final maxHeight = MediaQuery.of(context).size.height * 0.6; - - context.pop(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - showDoneButton: true, - showHeader: true, - showRemoveButton: true, - title: LocaleKeys.pageStyle_unsplash.tr(), - backgroundColor: backgroundColor, - onRemove: () { - pageStyleBloc.add( - DocumentPageStyleEvent.updateCoverImage( - PageStyleCover.none(), - ), - ); - }, - builder: (_) { - return ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxHeight, minHeight: 80), - child: BlocProvider.value( - value: pageStyleBloc, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: UnsplashImageWidget( - type: UnsplashImageType.fullScreen, - onSelectUnsplashImage: (url) { - pageStyleBloc.add( - DocumentPageStyleEvent.updateCoverImage( - PageStyleCover( - type: PageStyleCoverImageType.unsplashImage, - value: url, - ), - ), - ); - }, - ), - ), - ), - ); - }, - ); - } -} - -class _UnsplashCover extends StatelessWidget { - const _UnsplashCover(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg(FlowySvgs.m_page_style_unsplash_m), - const VSpace(4.0), - FlowyText( - LocaleKeys.pageStyle_unsplash.tr(), - fontSize: 12.0, - ), - ], - ); - } -} - -class _PhotoCover extends StatelessWidget { - const _PhotoCover(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg(FlowySvgs.m_page_style_photo_m), - const VSpace(4.0), - FlowyText( - LocaleKeys.pageStyle_photo.tr(), - fontSize: 12.0, - ), - ], - ); - } -} - -class _PresetCover extends StatelessWidget { - const _PresetCover(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg( - FlowySvgs.m_page_style_presets_m, - blendMode: null, - ), - const VSpace(4.0), - FlowyText( - LocaleKeys.pageStyle_presets.tr(), - fontSize: 12.0, - ), - ], - ); - } -} - -class _CoverOptionButton extends StatelessWidget { - const _CoverOptionButton({ - required this.showLeftCorner, - required this.showRightCorner, - required this.child, - required this.onTap, - required this.selected, - }); - - final Widget child; - final bool showLeftCorner; - final bool showRightCorner; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Expanded( - child: FeedbackGestureDetector( - feedbackType: HapticFeedbackType.medium, - onTap: onTap, - child: AnimatedContainer( - height: 64, - duration: Durations.medium1, - decoration: selected - ? ShapeDecoration( - color: const Color(0x141AC3F2), - shape: RoundedRectangleBorder( - side: const BorderSide( - width: 1.50, - color: Color(0xFF1AC3F2), - ), - borderRadius: BorderRadius.circular(12), - ), - ) - : null, - alignment: Alignment.center, - child: child, - ), - ), - ); - } -} 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 deleted file mode 100644 index a71b083c81..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.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/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/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'; - -class PageStyleIcon extends StatefulWidget { - const PageStyleIcon({ - super.key, - required this.view, - required this.tabs, - }); - - final ViewPB view; - final List tabs; - - @override - State createState() => _PageStyleIconState(); -} - -class _PageStyleIconState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => PageStyleIconBloc(view: widget.view) - ..add(const PageStyleIconEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final icon = state.icon; - return GestureDetector( - onTap: () => icon == null ? null : _showIconSelector(context, icon), - behavior: HitTestBehavior.opaque, - child: Container( - height: 52, - decoration: BoxDecoration( - color: context.pageStyleBackgroundColor, - borderRadius: BorderRadius.circular(12.0), - ), - child: Row( - children: [ - 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, - ), - const HSpace(6.0), - const FlowySvg(FlowySvgs.m_page_style_arrow_right_s), - const HSpace(12.0), - ], - ), - ), - ); - }, - ), - ); - } - - void _showIconSelector(BuildContext context, EmojiIconData icon) { - Navigator.pop(context); - final pageStyleIconBloc = PageStyleIconBloc(view: widget.view) - ..add(const PageStyleIconEvent.initial()); - 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: (ctx, 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); - }, - ), - ), - ); - }, - builder: (_) => const SizedBox.shrink(), - ); - } -} 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 deleted file mode 100644 index b2cd77f312..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart +++ /dev/null @@ -1,77 +0,0 @@ -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'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part '_page_style_icon_bloc.freezed.dart'; - -class PageStyleIconBloc extends Bloc { - PageStyleIconBloc({ - required this.view, - }) : _viewListener = ViewListener(viewId: view.id), - super(PageStyleIconState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - add( - PageStyleIconEvent.updateIcon( - view.icon.toEmojiIconData(), - false, - ), - ); - _viewListener?.start( - onViewUpdated: (view) { - add( - PageStyleIconEvent.updateIcon( - view.icon.toEmojiIconData(), - false, - ), - ); - }, - ); - }, - updateIcon: (icon, shouldUpdateRemote) async { - emit(state.copyWith(icon: icon)); - if (shouldUpdateRemote && icon != null) { - await ViewBackendService.updateViewIcon( - view: view, - viewIcon: icon, - ); - } - }, - ); - }, - ); - } - - final ViewPB view; - final ViewListener? _viewListener; - - @override - Future close() { - _viewListener?.stop(); - return super.close(); - } -} - -@freezed -class PageStyleIconEvent with _$PageStyleIconEvent { - const factory PageStyleIconEvent.initial() = Initial; - - const factory PageStyleIconEvent.updateIcon( - EmojiIconData? icon, - bool shouldUpdateRemote, - ) = UpdateIconInner; -} - -@freezed -class PageStyleIconState with _$PageStyleIconState { - const factory PageStyleIconState({ - @Default(null) EmojiIconData? icon, - }) = _PageStyleIconState; - - factory PageStyleIconState.initial() => const PageStyleIconState(); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart deleted file mode 100644 index 211e287d15..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart +++ /dev/null @@ -1,245 +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/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; -import 'package:appflowy/shared/feedback_gesture_detector.dart'; -import 'package:appflowy/util/font_family_extension.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.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'; - -const kPageStyleLayoutHeight = 52.0; - -class PageStyleLayout extends StatelessWidget { - const PageStyleLayout({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - Row( - children: [ - _OptionGroup( - options: const [ - PageStyleFontLayout.small, - PageStyleFontLayout.normal, - PageStyleFontLayout.large, - ], - selectedOption: state.fontLayout, - onTap: (option) => context - .read() - .add(DocumentPageStyleEvent.updateFont(option)), - ), - const HSpace(14), - _OptionGroup( - options: const [ - PageStyleLineHeightLayout.small, - PageStyleLineHeightLayout.normal, - PageStyleLineHeightLayout.large, - ], - selectedOption: state.lineHeightLayout, - onTap: (option) => context - .read() - .add(DocumentPageStyleEvent.updateLineHeight(option)), - ), - ], - ), - const VSpace(12.0), - const _FontButton(), - ], - ); - }, - ); - } -} - -class _OptionGroup extends StatelessWidget { - const _OptionGroup({ - required this.options, - required this.selectedOption, - required this.onTap, - }); - - final List options; - final T selectedOption; - final void Function(T option) onTap; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: context.pageStyleBackgroundColor, - borderRadius: const BorderRadius.horizontal( - left: Radius.circular(12), - right: Radius.circular(12), - ), - ), - child: Row( - children: options.map((option) { - final child = _buildSvg(option); - final showLeftCorner = option == options.first; - final showRightCorner = option == options.last; - return _buildOptionButton( - child, - showLeftCorner, - showRightCorner, - selectedOption == option, - () => onTap(option), - ); - }).toList(), - ), - ), - ); - } - - Widget _buildOptionButton( - Widget child, - bool showLeftCorner, - bool showRightCorner, - bool selected, - VoidCallback onTap, - ) { - return Expanded( - child: FeedbackGestureDetector( - feedbackType: HapticFeedbackType.medium, - onTap: onTap, - child: AnimatedContainer( - height: kPageStyleLayoutHeight, - duration: Durations.medium1, - decoration: selected - ? ShapeDecoration( - color: const Color(0x141AC3F2), - shape: RoundedRectangleBorder( - side: const BorderSide( - width: 1.50, - color: Color(0xFF1AC3F2), - ), - borderRadius: BorderRadius.circular(12), - ), - ) - : null, - alignment: Alignment.center, - child: child, - ), - ), - ); - } - - Widget _buildSvg(dynamic option) { - if (option is PageStyleFontLayout) { - return switch (option) { - PageStyleFontLayout.small => - const FlowySvg(FlowySvgs.m_font_size_small_s), - PageStyleFontLayout.normal => - const FlowySvg(FlowySvgs.m_font_size_normal_s), - PageStyleFontLayout.large => - const FlowySvg(FlowySvgs.m_font_size_large_s), - }; - } else if (option is PageStyleLineHeightLayout) { - return switch (option) { - PageStyleLineHeightLayout.small => - const FlowySvg(FlowySvgs.m_layout_small_s), - PageStyleLineHeightLayout.normal => - const FlowySvg(FlowySvgs.m_layout_normal_s), - PageStyleLineHeightLayout.large => - const FlowySvg(FlowySvgs.m_layout_large_s), - }; - } - throw ArgumentError('Invalid option type'); - } -} - -class _FontButton extends StatelessWidget { - const _FontButton(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final fontFamilyDisplayName = - (state.fontFamily ?? defaultFontFamily).fontFamilyDisplayName; - return GestureDetector( - onTap: () => _showFontSelector(context), - behavior: HitTestBehavior.opaque, - child: Container( - height: kPageStyleLayoutHeight, - decoration: BoxDecoration( - color: context.pageStyleBackgroundColor, - borderRadius: BorderRadius.circular(12.0), - ), - child: Row( - children: [ - const HSpace(16.0), - FlowyText(LocaleKeys.titleBar_font.tr()), - const Spacer(), - FlowyText( - fontFamilyDisplayName, - color: context.pageStyleTextColor, - ), - const HSpace(6.0), - const FlowySvg(FlowySvgs.m_page_style_arrow_right_s), - const HSpace(12.0), - ], - ), - ), - ); - }, - ); - } - - void _showFontSelector(BuildContext context) { - final pageStyleBloc = context.read(); - context.pop(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - showDoneButton: true, - showHeader: true, - title: LocaleKeys.titleBar_font.tr(), - backgroundColor: AFThemeExtension.of(context).background, - enableDraggableScrollable: true, - minChildSize: 0.6, - initialChildSize: 0.61, - scrollableWidgetBuilder: (_, controller) { - return BlocProvider.value( - value: pageStyleBloc, - child: BlocBuilder( - builder: (context, state) { - return Expanded( - child: Scrollbar( - controller: controller, - child: FontSelector( - scrollController: controller, - selectedFontFamilyName: - state.fontFamily ?? defaultFontFamily, - onFontFamilySelected: (fontFamilyName) { - pageStyleBloc.add( - DocumentPageStyleEvent.updateFontFamily( - fontFamilyName, - ), - ); - }, - ), - ), - ); - }, - ), - ); - }, - builder: (_) => const SizedBox.shrink(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart deleted file mode 100644 index 101046fb93..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -extension PageStyleUtil on BuildContext { - Color get pageStyleBackgroundColor { - final themeMode = Theme.of(this).brightness; - return themeMode == Brightness.light - ? const Color(0xFFF5F5F8) - : const Color(0xFF303030); - } - - Color get pageStyleTextColor { - final themeMode = Theme.of(this).brightness; - return themeMode == Brightness.light - ? const Color(0x7F1F2225) - : Colors.white54; - } -} 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 deleted file mode 100644 index 013a056a7c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart'; -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'; -import 'package:flutter/material.dart'; - -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) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // cover image - FlowyText( - LocaleKeys.pageStyle_coverImage.tr(), - color: context.pageStyleTextColor, - fontSize: 14.0, - ), - const VSpace(8.0), - PageStyleCoverImage(documentId: view.id), - const VSpace(20.0), - // layout: font size, line height and font family. - FlowyText( - LocaleKeys.pageStyle_layout.tr(), - color: context.pageStyleTextColor, - fontSize: 14.0, - ), - const VSpace(8.0), - const PageStyleLayout(), - const VSpace(20.0), - // icon - FlowyText( - LocaleKeys.pageStyle_pageIcon.tr(), - color: context.pageStyleTextColor, - fontSize: 14.0, - ), - 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 deleted file mode 100644 index d9cf060e3b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart +++ /dev/null @@ -1,32 +0,0 @@ -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 { - const CalloutNodeParser(); - - @override - String get id => CalloutBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - final delta = node.delta ?? Delta() - ..insert(''); - final String markdown = DeltaMarkdownEncoder() - .convert(delta) - .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 - -'''; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart new file mode 100644 index 0000000000..88ec444dec --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart @@ -0,0 +1,13 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +class CodeBlockNodeParser extends NodeParser { + const CodeBlockNodeParser(); + + @override + String get id => 'code_block'; + + @override + String transform(Node node) { + return '```\n${node.attributes['code_block']}\n```'; + } +} 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 deleted file mode 100644 index d4b6bb444f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart +++ /dev/null @@ -1,84 +0,0 @@ -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(); - - @override - String get id => ImageBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - assert(node.children.isEmpty); - final url = node.attributes[CustomImageBlockKeys.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/divider_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart new file mode 100644 index 0000000000..901ed6adb4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart @@ -0,0 +1,13 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +class DividerNodeParser extends NodeParser { + const DividerNodeParser(); + + @override + String get id => 'divider'; + + @override + String transform(Node node) { + return '---\n'; + } +} 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 deleted file mode 100644 index c0a15629b8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 4ad7734643..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'markdown_code_parser.dart'; -export 'markdown_simple_table_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/math_equation_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart index fed895c978..3f72d12ce3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart @@ -8,7 +8,7 @@ class MathEquationNodeParser extends NodeParser { String get id => MathEquationBlockKeys.type; @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { + String transform(Node node) { return '\$\$${node.attributes[id]}\$\$'; } } 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/parsers/toggle_list_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/toggle_list_node_parser.dart deleted file mode 100644 index 3cf79a671a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/toggle_list_node_parser.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -enum ToggleListExportStyle { - github, - markdown, -} - -class ToggleListNodeParser extends NodeParser { - const ToggleListNodeParser({ - this.exportStyle = ToggleListExportStyle.markdown, - }); - - final ToggleListExportStyle exportStyle; - - @override - String get id => ToggleListBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - final delta = node.delta ?? Delta() - ..insert(''); - String markdown = DeltaMarkdownEncoder().convert(delta); - final details = encoder?.convertNodes( - node.children, - withIndent: true, - ); - switch (exportStyle) { - case ToggleListExportStyle.github: - return '''
-$markdown - -$details -
-'''; - case ToggleListExportStyle.markdown: - markdown = '- $markdown\n'; - if (details != null && details.isNotEmpty) { - markdown += details; - } - return markdown; - } - } -} 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..79267e2c32 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,78 +1,16 @@ -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 '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'; -export 'copy_and_paste/custom_paste_command.dart'; -export 'database/database_view_block_component.dart'; -export 'database/inline_database_menu_item.dart'; -export 'database/referenced_database_menu_item.dart'; -export 'error/error_block_component_builder.dart'; +export 'code_block/code_block_component.dart'; +export 'code_block/code_block_shortcut_event.dart'; +export 'cover/change_cover_popover_bloc.dart'; +export 'cover/cover_node_widget.dart'; +export 'cover/cover_image_picker.dart'; +export 'emoji_picker/emoji_menu_item.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 '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_menu.dart'; +export 'database/inline_database_menu_item.dart'; +export 'database/database_view_block_component.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/indent_outdent_toolbar_item.dart'; -export 'mobile_toolbar_v3/list_toolbar_item.dart'; -export 'mobile_toolbar_v3/more_toolbar_item.dart'; -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 '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 'openai/widgets/auto_completion_node_widget.dart'; +export 'openai/widgets/smart_edit_node_widget.dart'; +export 'openai/widgets/smart_edit_toolbar_item.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/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 deleted file mode 100644 index 0abba733fb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:math' as math; - -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'; - -const tableActions = [ - TableOptionAction.addAfter, - TableOptionAction.addBefore, - TableOptionAction.delete, - TableOptionAction.duplicate, - TableOptionAction.clear, - TableOptionAction.bgColor, -]; - -class TableMenu extends StatelessWidget { - const TableMenu({ - super.key, - required this.node, - required this.editorState, - required this.position, - required this.dir, - this.onBuild, - this.onClose, - }); - - final Node node; - final EditorState editorState; - final int position; - final TableDirection dir; - final VoidCallback? onBuild; - final VoidCallback? onClose; - - @override - Widget build(BuildContext context) { - final actions = tableActions.map((action) { - switch (action) { - case TableOptionAction.bgColor: - return TableColorOptionAction( - node: node, - editorState: editorState, - position: position, - dir: dir, - ); - default: - return TableOptionActionWrapper(action); - } - }).toList(); - - return PopoverActionList( - direction: dir == TableDirection.col - ? PopoverDirection.bottomWithCenterAligned - : PopoverDirection.rightWithTopAligned, - actions: actions, - onPopupBuilder: onBuild, - onClosed: onClose, - onSelected: (action, controller) { - if (action is TableOptionActionWrapper) { - _onSelectAction(action.inner); - controller.close(); - } - }, - buildChild: (controller) => _buildOptionButton(controller, context), - ); - } - - Widget _buildOptionButton( - PopoverController controller, - BuildContext context, - ) { - return Card( - elevation: 1.0, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => controller.show(), - child: Transform.rotate( - angle: dir == TableDirection.col ? math.pi / 2 : 0, - child: const FlowySvg( - FlowySvgs.drag_element_s, - size: Size.square(18.0), - ), - ), - ), - ), - ); - } - - void _onSelectAction(TableOptionAction action) { - switch (action) { - case TableOptionAction.addAfter: - TableActions.add(node, position + 1, editorState, dir); - break; - case TableOptionAction.addBefore: - TableActions.add(node, position, editorState, dir); - break; - case TableOptionAction.delete: - TableActions.delete(node, position, editorState, dir); - break; - case TableOptionAction.clear: - TableActions.clear(node, position, editorState, dir); - break; - case TableOptionAction.duplicate: - TableActions.duplicate(node, position, editorState, dir); - break; - default: - } - } -} 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 deleted file mode 100644 index 993ee9b5a7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart +++ /dev/null @@ -1,157 +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/extensions/flowy_tint_extension.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.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/extension.dart'; -import 'package:flutter/material.dart'; - -const tableCellDefaultColor = 'appflowy_table_cell_default_color'; - -enum TableOptionAction { - addAfter, - addBefore, - delete, - duplicate, - clear, - - /// row|cell background color - bgColor; - - Widget icon(Color? color) { - switch (this) { - case TableOptionAction.addAfter: - return const FlowySvg(FlowySvgs.add_s); - case TableOptionAction.addBefore: - return const FlowySvg(FlowySvgs.add_s); - case TableOptionAction.delete: - return const FlowySvg(FlowySvgs.delete_s); - case TableOptionAction.duplicate: - return const FlowySvg(FlowySvgs.copy_s); - case TableOptionAction.clear: - return const FlowySvg(FlowySvgs.close_s); - case TableOptionAction.bgColor: - return const FlowySvg( - FlowySvgs.color_format_m, - size: Size.square(12), - ).padding(all: 2.0); - } - } - - String get description { - switch (this) { - case TableOptionAction.addAfter: - return LocaleKeys.document_plugins_table_addAfter.tr(); - case TableOptionAction.addBefore: - return LocaleKeys.document_plugins_table_addBefore.tr(); - case TableOptionAction.delete: - return LocaleKeys.document_plugins_table_delete.tr(); - case TableOptionAction.duplicate: - return LocaleKeys.document_plugins_table_duplicate.tr(); - case TableOptionAction.clear: - return LocaleKeys.document_plugins_table_clear.tr(); - case TableOptionAction.bgColor: - return LocaleKeys.document_plugins_table_bgColor.tr(); - } - } -} - -class TableOptionActionWrapper extends ActionCell { - TableOptionActionWrapper(this.inner); - - final TableOptionAction inner; - - @override - Widget? leftIcon(Color iconColor) => inner.icon(iconColor); - - @override - String get name => inner.description; -} - -class TableColorOptionAction extends PopoverActionCell { - TableColorOptionAction({ - required this.node, - required this.editorState, - required this.position, - required this.dir, - }); - - final Node node; - final EditorState editorState; - final int position; - final TableDirection dir; - - @override - Widget? leftIcon(Color iconColor) => - TableOptionAction.bgColor.icon(iconColor); - - @override - String get name => TableOptionAction.bgColor.description; - - @override - Widget Function( - BuildContext context, - PopoverController parentController, - PopoverController controller, - ) get builder => (context, parentController, controller) { - int row = 0, col = position; - if (dir == TableDirection.row) { - col = 0; - row = position; - } - - final cell = node.children.firstWhereOrNull( - (n) => - n.attributes[TableCellBlockKeys.colPosition] == col && - n.attributes[TableCellBlockKeys.rowPosition] == row, - ); - final key = dir == TableDirection.col - ? TableCellBlockKeys.colBackgroundColor - : TableCellBlockKeys.rowBackgroundColor; - final bgColor = cell?.attributes[key] as String?; - final selectedColor = bgColor?.tryToColor(); - // get default background color from themeExtension - final defaultColor = AFThemeExtension.of(context).tableCellBGColor; - final colors = [ - // reset to default background color - FlowyColorOption( - color: defaultColor, - i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(), - id: tableCellDefaultColor, - ), - ...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 backgroundColor = - selectedColor != option.color ? option.id : ''; - TableActions.setBgColor( - node, - position, - editorState, - backgroundColor, - dir, - ); - - controller.close(); - parentController.close(); - }, - ); - }; -} 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 deleted file mode 100644 index 95841051d7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class TodoListIcon extends StatelessWidget { - const TodoListIcon({ - super.key, - required this.node, - required this.onCheck, - }); - - final Node node; - final VoidCallback onCheck; - - @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 checked = node.attributes[TodoListBlockKeys.checked] ?? false; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - HapticFeedback.lightImpact(); - onCheck(); - }, - child: Container( - constraints: BoxConstraints( - minWidth: iconSize, - minHeight: iconSize, - ), - margin: const EdgeInsets.only(right: 8.0), - alignment: Alignment.center, - 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..b356f1545c 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,7 @@ -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: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._(); @@ -15,87 +11,37 @@ class ToggleListBlockKeys { /// The content of a code block. /// /// The value is a String. - static const String delta = blockComponentDelta; - - static const String backgroundColor = blockComponentBackgroundColor; - - static const String textDirection = blockComponentTextDirection; + static const String delta = 'delta'; /// 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({ - String? text, Delta? delta, bool collapsed = false, - String? textDirection, - Attributes? attributes, - Iterable? children, }) { - delta ??= Delta()..insert(text ?? ''); + final attributes = { + ToggleListBlockKeys.delta: (delta ?? Delta()).toJson(), + ToggleListBlockKeys.collapsed: collapsed, + }; return Node( type: ToggleListBlockKeys.type, - children: children ?? [], - attributes: { - if (textDirection != null) - ToggleListBlockKeys.textDirection: textDirection, - ToggleListBlockKeys.collapsed: collapsed, - ToggleListBlockKeys.delta: delta.toJson(), - if (attributes != null) ...attributes, - }, + attributes: attributes, + children: [paragraphNode()], ); } -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, - }, - ); -} - -// defining the toggle list block menu item -SelectionMenuItem toggleListBlockItem = SelectionMenuItem.node( - getName: LocaleKeys.document_plugins_toggleList.tr, - iconData: Icons.arrow_right, - keywords: ['collapsed list', 'toggle list', 'list'], - nodeBuilder: (editorState, _) => toggleListBlockNode(), - replace: (_, node) => node.delta?.isEmpty ?? false, -); - class ToggleListBlockComponentBuilder extends BlockComponentBuilder { ToggleListBlockComponentBuilder({ - super.configuration, + this.configuration = const BlockComponentConfiguration(), this.padding = const EdgeInsets.all(0), - this.textStyleBuilder, }); - final EdgeInsets padding; + @override + final BlockComponentConfiguration configuration; - /// The text style of the toggle heading block. - final TextStyle Function(int level)? textStyleBuilder; + final EdgeInsets padding; @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -105,21 +51,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 +69,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() => @@ -146,12 +84,9 @@ class _ToggleListBlockComponentWidgetState extends State with SelectableMixin, - DefaultSelectableMixin, + DefaultSelectable, BlockComponentConfigurable, - BlockComponentBackgroundColorMixin, - NestedBlockComponentStatefulWidgetMixin, - BlockComponentTextDirectionMixin, - BlockComponentAlignMixin { + BackgroundColorMixin { // the key used to forward focus to the richtext child @override final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); @@ -162,97 +97,72 @@ class _ToggleListBlockComponentWidgetState @override GlobalKey> get containerKey => node.key; - @override - GlobalKey> blockComponentKey = GlobalKey( - debugLabel: ToggleListBlockKeys.type, - ); - @override Node get node => widget.node; - @override - EdgeInsets get indentPadding => configuration.indentPadding( - node, - calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ), - ); - bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false; - int? get level => node.attributes[ToggleListBlockKeys.level] as int?; + late final editorState = context.read(); @override Widget build(BuildContext context) { return collapsed - ? buildComponent(context) - : buildComponentWithChildren(context); + ? buildToggleListBlockComponent(context) + : buildToggleListBlockComponentWithChildren(context); } - @override - Widget buildComponentWithChildren(BuildContext context) { - return Stack( + Widget buildToggleListBlockComponentWithChildren(BuildContext context) { + return Container( + color: backgroundColor, + child: NestedListWidget( + children: editorState.renderer.buildList( + context, + widget.node.children, + ), + child: buildToggleListBlockComponent(context), + ), + ); + } + + // build the richtext child + Widget buildToggleListBlockComponent(BuildContext context) { + Widget child = Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (backgroundColor != Colors.transparent) - Positioned.fill( - left: cachedLeft, - top: padding.top, - child: Container( - width: double.infinity, - color: backgroundColor, - ), + // the emoji picker button for the note + FlowyIconButton( + width: 24.0, + icon: Icon( + collapsed ? Icons.arrow_right : Icons.arrow_drop_down, ), - Provider( - create: (context) => - DatabasePluginWidgetBuilderSize(horizontalPadding: 0.0), - child: NestedListWidget( - indentPadding: indentPadding, - child: buildComponent(context), - children: editorState.renderer.buildList( - context, - widget.node.children, + onPressed: onCollapsed, + ), + const SizedBox( + width: 4.0, + ), + Expanded( + child: FlowyRichText( + key: forwardKey, + node: widget.node, + editorState: editorState, + placeholderText: placeholderText, + lineHeight: 1.5, + textSpanDecorator: (textSpan) => textSpan.updateTextStyle( + textStyle, + ), + placeholderTextSpanDecorator: (textSpan) => + textSpan.updateTextStyle( + placeholderTextStyle, ), ), ), ], ); - } - @override - Widget buildComponent( - BuildContext context, { - bool withBackgroundColor = false, - }) { - Widget child = _buildToggleBlock(); - - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - blockColor: editorState.editorStyle.selectionColor, - supportTypes: const [ - BlockSelectionType.block, - ], - 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) { + if (widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } @@ -260,173 +170,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..d542902646 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart @@ -0,0 +1,24 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +const _greater = '>'; + +/// Convert '> ' to toggle list +/// +/// - support +/// - desktop +/// - mobile +/// - web +/// +CharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent( + key: 'format greater to quote', + character: ' ', + handler: (editorState) async => await formatMarkdownSymbol( + editorState, + (node) => node.type != ToggleListBlockKeys.type, + (_, text, __) => text == _greater, + (_, node, delta) => toggleListBlockNode( + delta: delta.compose(Delta()..delete(_greater.length)), + ), + ), +); 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 3664c9aee7..39240591a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -1,77 +1,23 @@ -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/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/plugins/document/presentation/more/cubit/document_appearance_cubit.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: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, }); 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(); @@ -79,559 +25,121 @@ class EditorStyleCustomizer { EditorStyle desktop() { final theme = Theme.of(context); - final afThemeExtension = AFThemeExtension.of(context); - final appearanceFont = context.read().state.font; - final appearance = context.read().state; - final fontSize = appearance.fontSize; - String fontFamily = appearance.fontFamily; - if (fontFamily.isEmpty && appearanceFont.isNotEmpty) { - fontFamily = appearanceFont; - } - - final cursorColor = (editorState?.editable ?? true) - ? (appearance.cursorColor ?? - DefaultAppearanceSettings.getDefaultCursorColor(context)) - : Colors.transparent; - + final fontSize = context.read().state.fontSize; return EditorStyle.desktop( padding: padding, - maxWidth: width, - cursorColor: cursorColor, - selectionColor: appearance.selectionColor ?? - DefaultAppearanceSettings.getDefaultSelectionColor(context), - defaultTextDirection: appearance.defaultTextDirection, + backgroundColor: theme.colorScheme.surface, + cursorColor: theme.colorScheme.primary, textStyleConfiguration: TextStyleConfiguration( - lineHeight: 1.4, - applyHeightToFirstAscent: true, - applyHeightToLastDescent: true, - text: baseTextStyle(fontFamily).copyWith( + text: TextStyle( + fontFamily: 'Poppins', fontSize: fontSize, - color: afThemeExtension.onBackground, + color: theme.colorScheme.onBackground, + height: 1.5, ), - bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith( + bold: const TextStyle( + fontFamily: 'Poppins-Bold', 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( + italic: const TextStyle(fontStyle: FontStyle.italic), + underline: const TextStyle(decoration: TextDecoration.underline), + strikethrough: const TextStyle(decoration: TextDecoration.lineThrough), + href: TextStyle( color: theme.colorScheme.primary, decoration: TextDecoration.underline, ), code: GoogleFonts.robotoMono( - textStyle: baseTextStyle(fontFamily).copyWith( + textStyle: TextStyle( fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, - backgroundColor: - theme.colorScheme.inverseSurface.withValues(alpha: 0.8), + backgroundColor: theme.colorScheme.inverseSurface, ), ), ), - textSpanDecorator: customizeAttributeDecorator, - textScaleFactor: - context.watch().state.textScaleFactor, - textSpanOverlayBuilder: _buildTextSpanOverlay, ); } EditorStyle mobile() { - final afThemeExtension = AFThemeExtension.of(context); - final pageStyle = context.read().state; final theme = Theme.of(context); - final fontSize = pageStyle.fontLayout.fontSize; - final lineHeight = pageStyle.lineHeightLayout.lineHeight; - final fontFamily = pageStyle.fontFamily ?? - context.read().state.font; - final defaultTextDirection = - context.read().state.defaultTextDirection; - final textScaleFactor = - context.read().state.textScaleFactor; - final baseTextStyle = this.baseTextStyle(fontFamily); - - return EditorStyle.mobile( + final fontSize = context.read().state.fontSize; + return EditorStyle.desktop( padding: padding, - defaultTextDirection: defaultTextDirection, + backgroundColor: theme.colorScheme.surface, + cursorColor: theme.colorScheme.primary, textStyleConfiguration: TextStyleConfiguration( - lineHeight: lineHeight, - text: baseTextStyle.copyWith( + text: TextStyle( + fontFamily: 'poppins', fontSize: fontSize, - color: afThemeExtension.onBackground, + color: theme.colorScheme.onBackground, + height: 1.5, ), - bold: baseTextStyle.copyWith(fontWeight: FontWeight.w600), - italic: baseTextStyle.copyWith(fontStyle: FontStyle.italic), - underline: baseTextStyle.copyWith(decoration: TextDecoration.underline), - strikethrough: baseTextStyle.copyWith( - decoration: TextDecoration.lineThrough, + bold: const TextStyle( + fontFamily: 'poppins-Bold', + fontWeight: FontWeight.w600, ), - href: baseTextStyle.copyWith( + italic: const TextStyle(fontStyle: FontStyle.italic), + underline: const TextStyle(decoration: TextDecoration.underline), + strikethrough: const TextStyle(decoration: TextDecoration.lineThrough), + href: TextStyle( color: theme.colorScheme.primary, decoration: TextDecoration.underline, ), code: GoogleFonts.robotoMono( - textStyle: baseTextStyle.copyWith( + textStyle: TextStyle( fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, - backgroundColor: Colors.grey.withValues(alpha: 0.3), + backgroundColor: theme.colorScheme.inverseSurface, ), ), - applyHeightToFirstAscent: true, - applyHeightToLastDescent: true, ), - textSpanDecorator: customizeAttributeDecorator, - magnifierSize: const Size(144, 96), - textScaleFactor: textScaleFactor, - mobileDragHandleLeftExtend: 12.0, - mobileDragHandleWidthExtend: 24.0, ); } TextStyle headingStyleBuilder(int level) { - final String? fontFamily; - final List fontSizes; - final double fontSize; - if (UniversalPlatform.isMobile) { - final state = context.read().state; - fontFamily = state.fontFamily; - fontSize = state.fontLayout.fontSize; - fontSizes = state.fontLayout.headingFontSizes; - } else { - fontFamily = context.read().state.fontFamily; - fontSize = context.read().state.fontSize; - fontSizes = [ - fontSize + 16, - fontSize + 12, - fontSize + 8, - fontSize + 4, - fontSize + 2, - fontSize, - ]; - } - return baseTextStyle(fontFamily, fontWeight: FontWeight.w600).copyWith( - fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize, - ); - } - - CodeBlockStyle 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), + final fontSizes = [ + fontSize + 16, + fontSize + 12, + fontSize + 8, + fontSize + 4, + fontSize + 2, + fontSize + ]; + return TextStyle( + fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize, + fontWeight: FontWeight.bold, ); } - 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, - ); - } - } - - TextStyle outlineBlockPlaceholderStyleBuilder() { + TextStyle codeBlockStyleBuilder() { + final theme = Theme.of(context); final fontSize = context.read().state.fontSize; return TextStyle( - fontFamily: defaultFontFamily, + fontFamily: 'poppins', fontSize: fontSize, height: 1.5, - color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6), + color: theme.colorScheme.onBackground, ); } - 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); return SelectionMenuStyle( selectionMenuBackgroundColor: theme.cardColor, - selectionMenuItemTextColor: afThemeExtension.onBackground, - selectionMenuItemIconColor: afThemeExtension.onBackground, + selectionMenuItemTextColor: theme.colorScheme.onBackground, + selectionMenuItemIconColor: theme.colorScheme.onBackground, 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, + selectionMenuItemSelectedColor: theme.hoverColor, ); } - InlineActionsMenuStyle inlineActionsMenuStyleBuilder() { + FloatingToolbarStyle floatingToolbarStyleBuilder() { final theme = Theme.of(context); - final afThemeExtension = AFThemeExtension.of(context); - return InlineActionsMenuStyle( - backgroundColor: theme.cardColor, - groupTextColor: afThemeExtension.onBackground.withValues(alpha: .8), - menuItemTextColor: afThemeExtension.onBackground, - menuItemSelectedColor: theme.colorScheme.secondary, - menuItemSelectedTextColor: theme.colorScheme.onSurface, + return FloatingToolbarStyle( + backgroundColor: theme.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 { - if ([defaultFontFamily, builtInCodeFontFamily].contains(fontFamily)) { - return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight); - } - - return TextStyle(fontWeight: fontWeight); - } - } - - InlineSpan customizeAttributeDecorator( - BuildContext context, - Node node, - int index, - TextInsert text, - TextSpan before, - TextSpan after, - ) { - final attributes = text.attributes; - if (attributes == null) { - 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, - attributes.backgroundColor!, - ); - if (color != null) { - return TextSpan( - text: before.text, - style: newStyle?.merge( - TextStyle(backgroundColor: color), - ), - ); - } - } - - // try to refresh font here. - if (attributes.fontFamily != null) { - try { - if (before.text?.contains('_regular') == true) { - getGoogleFontSafely(attributes.fontFamily!.parseFontFamilyName()); - } else { - return TextSpan( - text: before.text, - style: newStyle?.merge( - getGoogleFontSafely(attributes.fontFamily!), - ), - ); - } - } catch (_) { - // ignore - } - } - - // Inline Mentions (Page Reference, Date, Reminder, etc.) - final mention = - attributes[MentionBlockKeys.mention] as Map?; - if (mention != null) { - final type = mention[MentionBlockKeys.type]; - return WidgetSpan( - alignment: PlaceholderAlignment.middle, - style: newStyle, - child: MentionBlock( - key: ValueKey( - switch (type) { - MentionType.page => mention[MentionBlockKeys.pageId], - MentionType.date => mention[MentionBlockKeys.date], - _ => MentionBlockKeys.mention, - }, - ), - node: node, - index: index, - mention: mention, - textStyle: newStyle, - ), - ); - } - - // customize the inline math equation block - 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, - ), - ); - } - - // customize the link on mobile - final href = attributes[AppFlowyRichTextKeys.href] as String?; - if (UniversalPlatform.isMobile && href != null) { - return TextSpan( - style: before.style, - text: text.text, - recognizer: TapGestureRecognizer() - ..onTap = () { - final editorState = context.read(); - if (editorState.selection == null) { - afLaunchUrlString(href, addingHttpSchemeWhenFailed: true); - return; - } - - editorState.updateSelectionWithReason( - editorState.selection, - extraInfo: {selectionExtraInfoDisableMobileToolbarKey: true}, - ); - - showEditLinkBottomSheet( - context, - text.text, - href, - (linkContext, newText, newHref) { - final selection = Selection.single( - path: node.path, - startOffset: index, - endOffset: index + text.text.length, - ); - editorState.updateTextAndHref( - text.text, - href, - newText, - newHref, - selection: selection, - ); - linkContext.pop(); - }, - ); - }, - ); - } - - 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, - ); - - // 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/export_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart new file mode 100644 index 0000000000..1da2e9da99 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart @@ -0,0 +1,38 @@ +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class ExportPageWidget extends StatelessWidget { + const ExportPageWidget({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const FlowyText.medium( + 'Open document failed', + fontSize: 18.0, + ), + const VSpace(5), + const FlowyText.regular( + 'Please try to export the page and contact us.', + fontSize: 12.0, + ), + const VSpace(20), + RoundedTextButton( + title: 'Export page', + width: 100, + height: 30, + onPressed: onTap, + ) + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart new file mode 100644 index 0000000000..35cb598655 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/cubit/document_appearance_cubit.dart @@ -0,0 +1,44 @@ +import 'package:bloc/bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const String _kDocumentAppearanceFontSize = 'kDocumentAppearanceFontSize'; + +class DocumentAppearance { + const DocumentAppearance({ + required this.fontSize, + }); + + final double fontSize; + // Will be supported... + // final String fontName; + + DocumentAppearance copyWith({double? fontSize}) { + return DocumentAppearance( + fontSize: fontSize ?? this.fontSize, + ); + } +} + +class DocumentAppearanceCubit extends Cubit { + DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 16.0)); + + void fetch() async { + final prefs = await SharedPreferences.getInstance(); + final fontSize = prefs.getDouble(_kDocumentAppearanceFontSize) ?? 16.0; + emit( + state.copyWith( + fontSize: fontSize, + ), + ); + } + + void syncFontSize(double fontSize) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setDouble(_kDocumentAppearanceFontSize, fontSize); + emit( + state.copyWith( + fontSize: fontSize, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart new file mode 100644 index 0000000000..40328dc489 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class FontSizeSwitcher extends StatefulWidget { + const FontSizeSwitcher({ + super.key, + }); + + @override + State createState() => _FontSizeSwitcherState(); +} + +class _FontSizeSwitcherState extends State { + final List<(String, double, bool)> _fontSizes = [ + (LocaleKeys.moreAction_small.tr(), 14.0, false), + (LocaleKeys.moreAction_medium.tr(), 18.0, true), + (LocaleKeys.moreAction_large.tr(), 22.0, false), + ]; + + @override + Widget build(BuildContext context) { + final selectedBgColor = AFThemeExtension.of(context).toggleButtonBGColor; + final foregroundColor = Theme.of(context).colorScheme.onBackground; + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold( + LocaleKeys.moreAction_fontSize.tr(), + fontSize: 12, + color: Theme.of(context).colorScheme.tertiary, + ), + const SizedBox( + height: 5, + ), + ToggleButtons( + isSelected: + _fontSizes.map((e) => e.$2 == state.fontSize).toList(), + onPressed: (int index) { + _updateSelectedFontSize(_fontSizes[index].$2); + }, + color: foregroundColor, + borderRadius: const BorderRadius.all(Radius.circular(5)), + borderColor: foregroundColor, + borderWidth: 0.5, + // when selected + selectedColor: foregroundColor, + selectedBorderColor: foregroundColor, + fillColor: selectedBgColor, + // when hover + hoverColor: selectedBgColor.withOpacity(0.3), + constraints: const BoxConstraints( + minHeight: 40.0, + minWidth: 80.0, + ), + children: _fontSizes + .map( + (e) => Text( + e.$1, + style: TextStyle(fontSize: e.$2), + ), + ) + .toList(), + ), + ], + ); + }, + ); + } + + void _updateSelectedFontSize(double fontSize) { + context.read().syncFontSize(fontSize); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart new file mode 100644 index 0000000000..181ac2b225 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart @@ -0,0 +1,39 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DocumentMoreButton extends StatelessWidget { + const DocumentMoreButton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + color: Theme.of(context).colorScheme.surfaceVariant, + offset: const Offset(0, 30), + tooltip: LocaleKeys.moreAction_moreOptions.tr(), + itemBuilder: (context) { + return [ + PopupMenuItem( + value: 1, + enabled: false, + child: BlocProvider.value( + value: context.read(), + child: const FontSizeSwitcher(), + ), + ), + ]; + }, + child: svgWidget( + 'editor/details', + size: const Size(18, 18), + color: Theme.of(context).iconTheme.color, + ), + ); + } +} 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..162315a5e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -0,0 +1,161 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/plugins/document/application/share_bloc.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.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-document2/entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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) => ConstrainedBox( + constraints: const BoxConstraints.expand( + height: 30, + width: 100, + ), + 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; + } + } + + 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) { + return RoundedTextButton( + title: LocaleKeys.shareAction_buttonText.tr(), + onPressed: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + switch (action.inner) { + case ShareAction.markdown: + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '$name.md', + ); + if (exportPath != null) { + docShareBloc.add(DocShareEvent.shareMarkdown(exportPath)); + } + break; + } + controller.close(); + }, + ); + } + + void listenOnViewUpdated() { + name = widget.view.name; + viewListener.start( + onViewUpdated: (view) { + name = view.name; + }, + ); + } +} + +enum ShareAction { + markdown, +} + +class ShareActionWrapper extends ActionCell { + final ShareAction inner; + + ShareActionWrapper(this.inner); + + Widget? icon(Color iconColor) => null; + + @override + String get name { + switch (inner) { + case ShareAction.markdown: + return LocaleKeys.shareAction_markdown.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 deleted file mode 100644 index 747c8667f8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart +++ /dev/null @@ -1,214 +0,0 @@ -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'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -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(), -]; - -class DateReferenceService extends InlineActionsDelegate { - DateReferenceService(this.context) { - // Initialize locale - _locale = context.locale.toLanguageTag(); - - // Initializes options - _setOptions(); - } - - final BuildContext context; - - late String _locale; - late List _allOptions; - - List options = []; - - @override - Future search([ - String? search, - ]) async { - // Checks if Locale has changed since last - _setLocale(); - - // Filters static options - _filterOptions(search); - - // Searches for date by pattern - _searchDate(search); - - // Searches for date by natural language prompt - await _searchDateNLP(search); - - return InlineActionsResult( - title: LocaleKeys.inlineActions_date.tr(), - results: options, - ); - } - - void _filterOptions(String? search) { - if (search == null || search.isEmpty) { - options = _allOptions; - return; - } - - options = _allOptions - .where( - (option) => - option.keywords != null && - option.keywords!.isNotEmpty && - option.keywords!.any( - (keyword) => keyword.contains(search.toLowerCase()), - ), - ) - .toList(); - - if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) { - _setOptions(); - options = _allOptions; - } - } - - void _searchDate(String? search) { - if (search == null || search.isEmpty) { - return; - } - - try { - final date = DateFormat.yMd(_locale).parse(search); - options.insert(0, _itemFromDate(date)); - } catch (_) { - return; - } - } - - Future _searchDateNLP(String? search) async { - if (search == null || search.isEmpty) { - return; - } - - final result = await DateService.queryDate(search); - - result.fold( - (date) => options.insert(0, _itemFromDate(date)), - (_) {}, - ); - } - - Future _insertDateReference( - EditorState editorState, - DateTime date, - int start, - int end, - ) 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, - start, - end, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - includeTime: false, - reminderId: null, - reminderOption: null, - ), - ); - - await editorState.apply(transaction); - } - - void _setOptions() { - final today = DateTime.now(); - 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( - today, - LocaleKeys.relativeDates_today.tr(), - [DateFormat.yMd(_locale).format(today)], - ); - tomorrowItem = _itemFromDate( - tomorrow, - LocaleKeys.relativeDates_tomorrow.tr(), - [DateFormat.yMd(_locale).format(tomorrow)], - ); - yesterdayItem = _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, - ]; - } - - /// Sets Locale on each search to make sure - /// keywords are localized - void _setLocale() { - final locale = context.locale.toLanguageTag(); - - if (locale != _locale) { - _locale = locale; - _setOptions(); - } - } - - InlineActionsMenuItem _itemFromDate( - DateTime date, [ - 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); - } - } - - return InlineActionsMenuItem( - label: labelStr.capitalize(), - keywords: [labelStr.toLowerCase(), ...?keywords], - onSelected: (context, editorState, menuService, replace) => - _insertDateReference( - editorState, - date, - replace.$1, - replace.$2, - ), - ); - } -} 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 deleted file mode 100644 index 9853d6757c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ /dev/null @@ -1,266 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.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'; -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'; -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:flutter/material.dart'; - -// const _channel = "InlinePageReference"; - -// TODO(Mathias): Clean up and use folder search instead -class InlinePageReferenceService extends InlineActionsDelegate { - InlinePageReferenceService({ - required this.currentViewId, - this.viewLayout, - this.customTitle, - this.insertPage = false, - this.limitResults = 5, - }) : assert(limitResults > 0, 'limitResults must be greater than 0') { - init(); - } - - final Completer _initCompleter = Completer(); - - final String currentViewId; - final ViewLayoutPB? viewLayout; - final String? customTitle; - - /// Defaults to false, if set to true the Page - /// will be inserted as a Reference - /// When false, a link to the view will be inserted - /// - final bool insertPage; - - /// Defaults to 5 - /// Will limit the page reference results - /// to [limitResults]. - /// - final int limitResults; - - late final CachedRecentService _recentService; - - bool _recentViewsInitialized = false; - late final List _recentViews; - - Future> _getRecentViews() async { - if (_recentViewsInitialized) { - return _recentViews; - } - - _recentViewsInitialized = true; - - final sectionViews = await _recentService.recentViews(); - final views = - sectionViews.unique((e) => e.item.id).map((e) => e.item).toList(); - - // Filter by viewLayout - views.retainWhere( - (i) => - currentViewId != i.id && - (viewLayout == null || i.layout == viewLayout), - ); - - // Map to InlineActionsMenuItem, then take 5 items - return _recentViews = views.map(_fromView).take(5).toList(); - } - - bool _viewsInitialized = false; - late final List _allViews; - - Future> _getViews() async { - if (_viewsInitialized) { - return _allViews; - } - - _viewsInitialized = true; - - final viewResult = await ViewBackendService.getAllViews(); - return _allViews = viewResult - .toNullable() - ?.items - .where((v) => viewLayout == null || v.layout == viewLayout) - .toList() ?? - const []; - } - - Future init() async { - _recentService = getIt(); - // _searchListener.start(onResultsClosed: _onResults); - } - - @override - Future dispose() async { - if (!_initCompleter.isCompleted) { - _initCompleter.complete(); - } - - await super.dispose(); - } - - @override - Future search([ - String? search, - ]) async { - final isSearching = search != null && search.isNotEmpty; - - late List items; - if (isSearching) { - final allViews = await _getViews(); - - 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())), - ) - .take(limitResults) - .map((view) => _fromView(view)) - .toList(); - } else { - items = await _getRecentViews(); - } - - return InlineActionsResult( - title: customTitle?.isNotEmpty == true - ? customTitle! - : isSearching - ? LocaleKeys.inlineActions_pageReference.tr() - : LocaleKeys.inlineActions_recentPages.tr(), - results: items, - ); - } - - Future _onInsertPageRef( - ViewPB view, - BuildContext context, - EditorState editorState, - (int, int) replace, - ) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final node = editorState.getNodeAtPath(selection.start.path); - - if (node != null) { - // Delete search term - if (replace.$2 > 0) { - final transaction = editorState.transaction - ..deleteText(node, replace.$1, replace.$2); - await editorState.apply(transaction); - } - - // Insert newline before inserting referenced database - if (node.delta?.toPlainText().isNotEmpty == true) { - await editorState.insertNewLine(); - } - } - - try { - await editorState.insertReferencePage(view, view.layout); - } on FlowyError catch (e) { - if (context.mounted) { - return Dialogs.show( - context, - child: AppFlowyErrorPage( - error: e, - ), - ); - } - } - } - - Future _onInsertLinkRef( - ViewPB view, - BuildContext context, - EditorState editorState, - InlineActionsMenuService menuService, - (int, int) replace, - ) 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; - } - - // @page name -> $ - // preload the page infos - pageMemorizer[view.id] = view; - final transaction = editorState.transaction - ..replaceText( - node, - replace.$1, - replace.$2, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: view.id, - blockId: null, - ), - ); - - 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, - ); - }, - 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; -// } - -// 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 deleted file mode 100644 index 471f1c9211..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ /dev/null @@ -1,252 +0,0 @@ -import 'package:appflowy/date/date_service.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/base/string_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; -import 'package:appflowy/plugins/inline_actions/service_handler.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/protobuf/flowy-user/reminder.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:nanoid/nanoid.dart'; - -final _keywords = [ - LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(), - LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(), -]; - -class ReminderReferenceService extends InlineActionsDelegate { - ReminderReferenceService(this.context) { - // Initialize locale - _locale = context.locale.toLanguageTag(); - - // Initializes options - _setOptions(); - } - - final BuildContext context; - - late String _locale; - late List _allOptions; - - List options = []; - - @override - Future search([ - String? search, - ]) async { - // Checks if Locale has changed since last - _setLocale(); - - // Filters static options - _filterOptions(search); - - // Searches for date by pattern - _searchDate(search); - - // Searches for date by natural language prompt - await _searchDateNLP(search); - - return _groupFromResults(options); - } - - InlineActionsResult _groupFromResults([ - List? options, - ]) => - InlineActionsResult( - title: LocaleKeys.inlineActions_reminder_groupTitle.tr(), - results: options ?? [], - startsWithKeywords: [ - LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(), - LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(), - ], - ); - - void _filterOptions(String? search) { - if (search == null || search.isEmpty) { - options = _allOptions; - return; - } - - options = _allOptions - .where( - (option) => - option.keywords != null && - option.keywords!.isNotEmpty && - option.keywords!.any( - (keyword) => keyword.contains(search.toLowerCase()), - ), - ) - .toList(); - - if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) { - _setOptions(); - options = _allOptions; - } - } - - void _searchDate(String? search) { - if (search == null || search.isEmpty) { - return; - } - - try { - final date = DateFormat.yMd(_locale).parse(search); - options.insert(0, _itemFromDate(date)); - } catch (_) { - return; - } - } - - Future _searchDateNLP(String? search) async { - if (search == null || search.isEmpty) { - return; - } - - final result = await DateService.queryDate(search); - - result.fold( - (date) { - // Only insert dates in the future - if (DateTime.now().isBefore(date)) { - options.insert(0, _itemFromDate(date)); - } - }, - (_) {}, - ); - } - - Future _insertReminderReference( - EditorState editorState, - DateTime date, - int start, - int end, - ) 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 viewId = context.read().documentId; - final reminder = _reminderFromDate(date, viewId, node); - - final transaction = editorState.transaction - ..replaceText( - node, - start, - end, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - reminderId: reminder.id, - reminderOption: ReminderOption.atTimeOfEvent.name, - includeTime: false, - ), - ); - - await editorState.apply(transaction); - - if (context.mounted) { - context.read().add(ReminderEvent.add(reminder: reminder)); - } - } - - void _setOptions() { - final today = DateTime.now(); - 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( - tomorrow, - LocaleKeys.relativeDates_tomorrow.tr(), - [DateFormat.yMd(_locale).format(tomorrow)], - ); - } catch (e) { - todayItem = _itemFromDate(today); - } - - try { - oneWeekItem = _itemFromDate( - oneWeek, - LocaleKeys.relativeDates_oneWeek.tr(), - [DateFormat.yMd(_locale).format(oneWeek)], - ); - } catch (e) { - oneWeekItem = _itemFromDate(oneWeek); - } - - _allOptions = [ - todayItem, - oneWeekItem, - ]; - } - - /// Sets Locale on each search to make sure - /// keywords are localized - void _setLocale() { - final locale = context.locale.toLanguageTag(); - - if (locale != _locale) { - _locale = locale; - _setOptions(); - } - } - - InlineActionsMenuItem _itemFromDate( - DateTime date, [ - 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); - } - } - - return InlineActionsMenuItem( - label: labelStr.capitalize(), - keywords: [labelStr.toLowerCase(), ...?keywords], - onSelected: (context, editorState, menuService, replace) => - _insertReminderReference(editorState, date, replace.$1, replace.$2), - ); - } - - ReminderPB _reminderFromDate(DateTime date, String viewId, Node node) { - return ReminderPB( - id: nanoid(), - objectId: viewId, - title: LocaleKeys.reminderNotification_title.tr(), - message: LocaleKeys.reminderNotification_message.tr(), - 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 deleted file mode 100644 index e0e03e7dec..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart +++ /dev/null @@ -1,83 +0,0 @@ -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 = '@'; - -CharacterShortcutEvent inlineActionsCommand( - InlineActionsService inlineActionsService, { - InlineActionsMenuStyle style = const InlineActionsMenuStyle.light(), -}) => - CharacterShortcutEvent( - key: 'Opens Inline Actions Menu', - character: inlineActionCharacter, - handler: (editorState) => inlineActionsCommandHandler( - editorState, - inlineActionsService, - style, - ), - ); - -InlineActionsMenuService? selectionMenuService; - -Future inlineActionsCommandHandler( - EditorState editorState, - InlineActionsService service, - InlineActionsMenuStyle style, -) async { - final selection = editorState.selection; - if (selection == null) { - return false; - } - - if (!selection.isCollapsed) { - await editorState.deleteSelection(selection); - } - - await editorState.insertTextAtPosition( - inlineActionCharacter, - position: selection.start, - ); - - final List initialResults = []; - for (final handler in service.handlers) { - final group = await handler.search(null); - - if (group.results.isNotEmpty) { - initialResults.add(group); - } - } - - 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, - ); - - // disable the keyboard service - editorState.service.keyboardService?.disable(); - - await selectionMenuService?.show(); - - // enable the keyboard service - editorState.service.keyboardService?.enable(); - } - - 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 deleted file mode 100644 index 651e739abc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'dart:async'; - -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 dismiss(); -} - -class InlineActionsMenu extends InlineActionsMenuService { - InlineActionsMenu({ - required this.context, - required this.editorState, - required this.service, - 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; - - final int startCharAmount; - - OverlayEntry? _menuEntry; - bool selectionChangedByMenu = false; - - @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); - } - } - - void _onSelectionUpdate() => selectionChangedByMenu = true; - - @override - Future show() { - final completer = Completer(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _show(); - completer.complete(); - }); - return completer.future; - } - - void _show() { - dismiss(); - - final selectionService = editorState.service.selectionService; - final selectionRects = selectionService.selectionRects; - if (selectionRects.isEmpty) { - return; - } - - const double menuHeight = 300.0; - const double menuWidth = 200.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, - ); - } - - // Show on the left - final windowWidth = MediaQuery.of(context).size.width; - if (offset.dx > (windowWidth - menuWidth)) { - alignment = alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - offset = Offset( - windowWidth - offset.dx, - offset.dy, - ); - } - - final (left, top, right, bottom) = _getPosition(alignment, offset); - - _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: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: InlineActionsHandler( - service: service, - results: initialResults, - editorState: editorState, - menuService: this, - onDismiss: dismiss, - onSelectionUpdate: _onSelectionUpdate, - style: style, - startCharAmount: startCharAmount, - cancelBySpaceHandler: cancelBySpaceHandler, - ), - ), - ), - ], - ), - ), - ), - ); - - Overlay.of(context).insert(_menuEntry!); - - 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( - Alignment alignment, - Offset offset, - ) { - 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); - } -} - -class InlineActionsMenuStyle { - InlineActionsMenuStyle({ - required this.backgroundColor, - required this.groupTextColor, - required this.menuItemTextColor, - required this.menuItemSelectedColor, - required this.menuItemSelectedTextColor, - }); - - const InlineActionsMenuStyle.light() - : backgroundColor = Colors.white, - groupTextColor = const Color(0xFF555555), - menuItemTextColor = const Color(0xFF333333), - menuItemSelectedColor = const Color(0xFFE0F8FF), - menuItemSelectedTextColor = const Color.fromARGB(255, 56, 91, 247); - - const InlineActionsMenuStyle.dark() - : backgroundColor = const Color(0xFF282E3A), - groupTextColor = const Color(0xFFBBC3CD), - menuItemTextColor = const Color(0xFFBBC3CD), - menuItemSelectedColor = const Color(0xFF00BCF0), - menuItemSelectedTextColor = const Color(0xFF131720); - - /// The background color of the context menu itself - /// - final Color backgroundColor; - - /// The color of the [InlineActionsGroup]'s title text - /// - final Color groupTextColor; - - /// The text color of an [InlineActionsMenuItem] - /// - final Color menuItemTextColor; - - /// The background of the currently selected [InlineActionsMenuItem] - /// - final Color menuItemSelectedColor; - - /// The text color of the currently selected [InlineActionsMenuItem] - /// - final Color menuItemSelectedTextColor; -} 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 deleted file mode 100644 index 1fe2703870..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -typedef SelectItemHandler = void Function( - BuildContext context, - EditorState editorState, - InlineActionsMenuService menuService, - (int start, int end) replacement, -); - -class InlineActionsMenuItem { - InlineActionsMenuItem({ - required this.label, - this.iconBuilder, - this.keywords, - this.onSelected, - }); - - final String label; - final Widget Function(bool onSelected)? iconBuilder; - final List? keywords; - final SelectItemHandler? onSelected; -} - -class InlineActionsResult { - InlineActionsResult({ - this.title, - required this.results, - this.startsWithKeywords, - }); - - /// Localized title to be displayed above the results - /// of the current group. - /// - /// If null, no title will be displayed. - /// - final String? title; - - /// List of results that will be displayed for this group - /// made up of [SelectionMenuItem]s. - /// - final List results; - - /// If the search term start with one of these keyword, - /// the results will be reordered such that these results - /// will be above. - /// - final List? startsWithKeywords; -} diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart deleted file mode 100644 index 3bdd5bf61a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/plugins/inline_actions/service_handler.dart'; - -abstract class _InlineActionsProvider { - void dispose(); -} - -class InlineActionsService extends _InlineActionsProvider { - InlineActionsService({ - required this.context, - required this.handlers, - }); - - /// The [BuildContext] in which to show the [InlineActionsMenu] - /// - BuildContext? context; - - final List handlers; - - /// This is a workaround for not having a mounted check. - /// Thus when the widget that uses the service is disposed, - /// we set the [BuildContext] to null. - /// - @override - Future dispose() async { - for (final handler in handlers) { - await handler.dispose(); - } - context = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart deleted file mode 100644 index de537fb964..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; - -abstract class InlineActionsDelegate { - Future search(String? search); - - Future dispose() async {} -} 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 deleted file mode 100644 index 63ccb04839..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ /dev/null @@ -1,468 +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/plugins/inline_actions/widgets/inline_actions_menu_group.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 '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 _contentHeight = 260; - -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 InlineActionsHandler extends StatefulWidget { - const InlineActionsHandler({ - super.key, - required this.service, - required this.results, - required this.editorState, - required this.menuService, - required this.onDismiss, - required this.onSelectionUpdate, - required this.style, - this.startCharAmount = 1, - this.cancelBySpaceHandler, - }); - - final InlineActionsService service; - final List results; - final EditorState editorState; - final InlineActionsMenuService menuService; - final VoidCallback onDismiss; - final VoidCallback onSelectionUpdate; - final InlineActionsMenuStyle style; - final int startCharAmount; - final bool Function()? cancelBySpaceHandler; - - @override - State createState() => _InlineActionsHandlerState(); -} - -class _InlineActionsHandlerState extends State { - final _focusNode = FocusNode(debugLabel: 'inline_actions_menu_handler'); - final _scrollController = ScrollController(); - - 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 widget.editorState - .updateSelectionWithReason(widget.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 = widget.editorState.selection?.endIndex ?? 0; - } - - @override - void dispose() { - _scrollController.dispose(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Focus( - focusNode: _focusNode, - onKeyEvent: onKeyEvent, - child: Container( - constraints: const BoxConstraints( - maxHeight: kInlineMenuHeight, - minWidth: kInlineMenuWidth, - ), - 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: Padding( - padding: const EdgeInsets.all(8.0), - child: noResults - ? SizedBox( - width: 150, - child: FlowyText.regular( - LocaleKeys.inlineActions_noResults.tr(), - ), - ) - : SingleChildScrollView( - controller: _scrollController, - physics: const ClampingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: results - .where((g) => g.results.isNotEmpty) - .mapIndexed( - (index, group) => InlineActionsGroup( - result: group, - editorState: widget.editorState, - menuService: widget.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, - ), - ) - .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]; - - KeyEventResult onKeyEvent(focus, KeyEvent event) { - if (event is! KeyDownEvent) { - return KeyEventResult.ignored; - } - - const moveKeys = [ - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.tab, - ]; - - if (event.logicalKey == LogicalKeyboardKey.enter) { - if (_selectedGroup <= groupLength && - _selectedIndex <= lengthOfGroup(_selectedGroup)) { - handlerOf(_selectedGroup, _selectedIndex).onSelected?.call( - context, - widget.editorState, - widget.menuService, - ( - startOffset - widget.startCharAmount, - _search.length + widget.startCharAmount - ), - ); - - widget.onDismiss(); - return KeyEventResult.handled; - } - - if (noResults) { - // Workaround to bring focus back to editor - widget.editorState - .updateSelectionWithReason(widget.editorState.selection); - widget.editorState.insertNewLine(); - - widget.onDismiss(); - return KeyEventResult.handled; - } - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - // Workaround to bring focus back to editor - widget.editorState - .updateSelectionWithReason(widget.editorState.selection); - - widget.onDismiss(); - } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - if (_search.isEmpty) { - if (_canDeleteLastCharacter()) { - widget.editorState.deleteBackward(); - } else { - // Workaround for editor regaining focus - widget.editorState.apply( - widget.editorState.transaction - ..afterSelection = widget.editorState.selection, - ); - } - widget.onDismiss(); - } else { - widget.onSelectionUpdate(); - widget.editorState.deleteBackward(); - _deleteCharacterAtSelection(); - } - - return KeyEventResult.handled; - } else if (event.character != null && - ![ - ...moveKeys, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight, - ].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; - } - - if ([LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight] - .contains(event.logicalKey)) { - widget.onSelectionUpdate(); - - event.logicalKey == LogicalKeyboardKey.arrowLeft - ? widget.editorState.moveCursorForward() - : widget.editorState.moveCursorBackward(SelectionMoveRange.character); - - /// If cursor moves before @ then dismiss menu - /// If cursor moves after @search.length then dismiss menu - final selection = widget.editorState.selection; - if (selection != null && - (selection.endIndex < startOffset || - selection.endIndex > (startOffset + _search.length))) { - widget.onDismiss(); - } - - /// Workaround: When using the move cursor methods, it seems the - /// focus goes back to the editor, this makes sure this handler - /// receives the next keypress. - /// - _focusNode.requestFocus(); - - return KeyEventResult.handled; - } - - return KeyEventResult.handled; - } - - 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) { - bool didChange = false; - - if (key == LogicalKeyboardKey.arrowUp || - (key == LogicalKeyboardKey.tab && - HardwareKeyboard.instance.isShiftPressed)) { - if (_selectedIndex == 0 && _selectedGroup > 0) { - _selectedGroup -= 1; - _selectedIndex = lengthOfGroup(_selectedGroup) - 1; - didChange = true; - } else if (_selectedIndex > 0) { - _selectedIndex -= 1; - didChange = true; - } - } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab] - .contains(key)) { - if (_selectedIndex < lengthOfGroup(_selectedGroup) - 1) { - _selectedIndex += 1; - didChange = true; - } else if (_selectedGroup < groupLength - 1) { - _selectedGroup += 1; - _selectedIndex = 0; - didChange = true; - } - } - - if (mounted && didChange) { - setState(() {}); - _scrollToItem(); - } - } - - void _scrollToItem() { - final groups = _selectedGroup + 1; - - int items = 0; - for (int i = 0; i <= _selectedGroup; i++) { - items += lengthOfGroup(i); - } - - // Remove the leftover items - items -= lengthOfGroup(_selectedGroup) - (_selectedIndex + 1); - - /// The offset is roughly calculated by: - /// - Amount of Groups passed - /// - Amount of Items passed - final double offset = - (_groupTextHeight + _groupBottomSpacing) * groups + _itemHeight * items; - - // We have a buffer so that when moving up, we show items above the currently - // selected item. The buffer is the height of 2 items - if (offset <= _scrollController.offset + _itemHeight * 2) { - // We want to show the user some options above the newly - // focused one, therefore we take the offset and subtract - // the height of three items (current + 2) - _scrollController.animateTo( - offset - _itemHeight * 3, - duration: const Duration(milliseconds: 200), - curve: Curves.easeIn, - ); - } else if (offset > - _scrollController.offset + - _contentHeight - - _itemHeight - - _groupTextHeight) { - // The same here, we want to show the options below the - // newly focused item when moving downwards, therefore we add - // 2 times the item height to the offset - _scrollController.animateTo( - offset - _contentHeight + _itemHeight * 2, - duration: const Duration(milliseconds: 200), - curve: Curves.easeIn, - ); - } - } - - 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 - 1 + _search.length, - ); - } - - 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; - } -} 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 deleted file mode 100644 index 123cfc1177..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart +++ /dev/null @@ -1,134 +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/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'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -class InlineActionsGroup extends StatelessWidget { - const InlineActionsGroup({ - super.key, - required this.result, - required this.editorState, - required this.menuService, - required this.style, - required this.onSelected, - required this.startOffset, - required this.endOffset, - 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 int startOffset; - final int endOffset; - - final bool isLastGroup; - final bool isGroupSelected; - final int selectedIndex; - - @override - Widget build(BuildContext context) { - return Padding( - padding: isLastGroup ? EdgeInsets.zero : const EdgeInsets.only(bottom: 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (result.title != null) ...[ - FlowyText.medium(result.title!, color: style.groupTextColor), - const SizedBox(height: 4), - ], - ...result.results.mapIndexed( - (index, item) => InlineActionsWidget( - item: item, - editorState: editorState, - menuService: menuService, - isSelected: isGroupSelected && index == selectedIndex, - style: style, - onSelected: onSelected, - startOffset: startOffset, - endOffset: endOffset, - ), - ), - ], - ), - ); - } -} - -class InlineActionsWidget extends StatefulWidget { - const InlineActionsWidget({ - 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 - State createState() => _InlineActionsWidgetState(); -} - -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, - 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, - ), - ), - ], - ), - onTap: _onPressed, - ), - ), - ); - } - - void _onPressed() { - widget.onSelected(); - widget.item.onSelected?.call( - context, - widget.editorState, - widget.menuService, - (widget.startOffset, widget.endOffset), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/callback_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/shared/callback_shortcuts.dart deleted file mode 100644 index 238b6bd85d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/callback_shortcuts.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -typedef AFBindingCallback = bool Function(); - -class AFCallbackShortcuts extends StatelessWidget { - const AFCallbackShortcuts({ - super.key, - required this.bindings, - required this.child, - }); - - // The bindings for the shortcuts - // - // The result of the callback will be used to determine if the event is handled - final Map bindings; - final Widget child; - - bool _applyKeyEventBinding(ShortcutActivator activator, KeyEvent event) { - if (activator.accepts(event, HardwareKeyboard.instance)) { - return bindings[activator]?.call() ?? false; - } - return false; - } - - @override - Widget build(BuildContext context) { - return Focus( - canRequestFocus: false, - skipTraversal: true, - onKeyEvent: (FocusNode node, KeyEvent event) { - KeyEventResult result = KeyEventResult.ignored; - for (final ShortcutActivator activator in bindings.keys) { - result = _applyKeyEventBinding(activator, event) - ? KeyEventResult.handled - : result; - } - return result; - }, - child: child, - ); - } -} 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/shared/sync_indicator.dart b/frontend/appflowy_flutter/lib/plugins/shared/sync_indicator.dart deleted file mode 100644 index 7932c8fc20..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/sync_indicator.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/sync/database_sync_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_sync_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class DocumentSyncIndicator extends StatelessWidget { - const DocumentSyncIndicator({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - DocumentSyncBloc(view: view)..add(const DocumentSyncEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - // don't show indicator if user is local - if (!state.shouldShowIndicator) { - return const SizedBox.shrink(); - } - final Color color; - final String hintText; - - if (!state.isNetworkConnected) { - color = Colors.grey; - hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr(); - } else { - switch (state.syncState) { - case DocumentSyncState.SyncFinished: - color = Colors.green; - hintText = LocaleKeys.newSettings_syncState_synced.tr(); - break; - case DocumentSyncState.Syncing: - case DocumentSyncState.InitSyncBegin: - color = Colors.yellow; - hintText = LocaleKeys.newSettings_syncState_syncing.tr(); - break; - default: - return const SizedBox.shrink(); - } - } - - return FlowyTooltip( - message: hintText, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), - width: 8, - height: 8, - ), - ); - }, - ), - ); - } -} - -class DatabaseSyncIndicator extends StatelessWidget { - const DatabaseSyncIndicator({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - DatabaseSyncBloc(view: view)..add(const DatabaseSyncEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - // don't show indicator if user is local - if (!state.shouldShowIndicator) { - return const SizedBox.shrink(); - } - final Color color; - final String hintText; - - if (!state.isNetworkConnected) { - color = Colors.grey; - hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr(); - } else { - switch (state.syncState) { - case DatabaseSyncState.SyncFinished: - color = Colors.green; - hintText = LocaleKeys.newSettings_syncState_synced.tr(); - break; - case DatabaseSyncState.Syncing: - case DatabaseSyncState.InitSyncBegin: - color = Colors.yellow; - hintText = LocaleKeys.newSettings_syncState_syncing.tr(); - break; - default: - return const SizedBox.shrink(); - } - } - - return FlowyTooltip( - message: hintText, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), - width: 8, - height: 8, - ), - ); - }, - ), - ); - } -} 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..e64d0d2a2b 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_bloc.dart @@ -1,40 +1,33 @@ -import 'package:appflowy/plugins/trash/application/trash_listener.dart'; -import 'package:appflowy/plugins/trash/application/trash_service.dart'; +import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/trash.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:appflowy/plugins/trash/application/trash_service.dart'; +import 'package:appflowy/plugins/trash/application/trash_listener.dart'; part 'trash_bloc.freezed.dart'; class TrashBloc extends Bloc { + final TrashService _service; + final TrashListener _listener; TrashBloc() : _service = TrashService(), _listener = TrashListener(), super(TrashState.init()) { - _dispatch(); - } - - final TrashService _service; - final TrashListener _listener; - - void _dispatch() { on((event, emit) async { await event.map( initial: (e) async { _listener.start(trashUpdated: _listenTrashUpdated); final result = await _service.readTrash(); - emit( result.fold( (object) => state.copyWith( objects: object.items, - successOrFailure: FlowyResult.success(null), + successOrFailure: left(unit), ), - (error) => - state.copyWith(successOrFailure: FlowyResult.failure(error)), + (error) => state.copyWith(successOrFailure: right(error)), ), ); }, @@ -42,7 +35,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 { @@ -62,20 +55,18 @@ class TrashBloc extends Bloc { } Future _handleResult( - FlowyResult result, + Either result, Emitter emit, ) async { emit( result.fold( - (l) => state.copyWith(successOrFailure: FlowyResult.success(null)), - (error) => state.copyWith(successOrFailure: FlowyResult.failure(error)), + (l) => state.copyWith(successOrFailure: left(unit)), + (error) => state.copyWith(successOrFailure: right(error)), ), ); } - void _listenTrashUpdated( - FlowyResult, FlowyError> trashOrFailed, - ) { + void _listenTrashUpdated(Either, FlowyError> trashOrFailed) { trashOrFailed.fold( (trash) { add(TrashEvent.didReceiveTrash(trash)); @@ -107,11 +98,11 @@ class TrashEvent with _$TrashEvent { class TrashState with _$TrashState { const factory TrashState({ required List objects, - required FlowyResult successOrFailure, + required Either successOrFailure, }) = _TrashState; factory TrashState.init() => TrashState( objects: [], - successOrFailure: FlowyResult.success(null), + successOrFailure: left(unit), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_listener.dart b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_listener.dart index 1d1a8f1ed8..cd187486f3 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_listener.dart @@ -1,16 +1,15 @@ import 'dart:async'; import 'dart:typed_data'; - import 'package:appflowy/core/notification/folder_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; +import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/trash.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; typedef TrashUpdatedCallback = void Function( - FlowyResult, FlowyError> trashOrFailed, + Either, FlowyError> trashOrFailed, ); class TrashListener { @@ -30,7 +29,7 @@ class TrashListener { void _observableCallback( FolderNotification ty, - FlowyResult result, + Either result, ) { switch (ty) { case FolderNotification.DidUpdateTrash: @@ -38,9 +37,9 @@ class TrashListener { result.fold( (payload) { final repeatedTrash = RepeatedTrashPB.fromBuffer(payload); - _trashUpdated!(FlowyResult.success(repeatedTrash.items)); + _trashUpdated!(left(repeatedTrash.items)); }, - (error) => _trashUpdated!(FlowyResult.failure(error)), + (error) => _trashUpdated!(right(error)), ); } break; 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..29fb9f13c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_service.dart @@ -1,35 +1,34 @@ import 'dart:async'; - +import 'package:dartz/dartz.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/trash.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/trash.pb.dart'; class TrashService { - Future> readTrash() { - return FolderEventListTrashItems().send(); + Future> readTrash() { + return FolderEventReadTrash().send(); } - static Future> putback(String trashId) { + Future> putback(String trashId) { final id = TrashIdPB.create()..id = trashId; - return FolderEventRestoreTrashItem(id).send(); + return FolderEventPutbackTrash(id).send(); } - Future> deleteViews(List trash) { + Future> deleteViews(List trash) { final items = trash.map((trash) { return TrashIdPB.create()..id = trash; }); final ids = RepeatedTrashIdPB(items: items); - return FolderEventPermanentlyDeleteTrashItem(ids).send(); + return FolderEventDeleteTrash(ids).send(); } - Future> restoreAll() { - return FolderEventRecoverAllTrashItems().send(); + Future> restoreAll() { + return FolderEventRestoreAllTrash().send(); } - Future> deleteAll() { - return FolderEventPermanentlyDeleteAllTrashItem().send(); + Future> deleteAll() { + return FolderEventDeleteAllTrash().send(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/trash/menu.dart b/frontend/appflowy_flutter/lib/plugins/trash/menu.dart new file mode 100644 index 0000000000..8708aa7563 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/trash/menu.dart @@ -0,0 +1,57 @@ +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/workspace/presentation/home/menu/menu.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/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:appflowy/generated/locale_keys.g.dart'; + +class MenuTrash extends StatelessWidget { + const MenuTrash({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: getIt().notifier, + builder: (context, value, child) { + return FlowyHover( + style: HoverStyle( + hoverColor: AFThemeExtension.of(context).greySelect, + ), + isSelected: () => getIt().latestOpenView == null, + child: SizedBox( + height: 26, + child: InkWell( + onTap: () { + getIt().latestOpenView = null; + getIt() + .setPlugin(makePlugin(pluginType: PluginType.trash)); + }, + child: _render(context), + ), + ).padding(horizontal: Insets.l), + ).padding(horizontal: 8); + }, + ); + } + + Widget _render(BuildContext context) { + return Row( + children: [ + const FlowySvg( + size: Size(16, 16), + name: 'home/trash', + ), + const HSpace(6), + FlowyText.medium(LocaleKeys.trash_text.tr()), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart index e1cfefeffa..89c124d217 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart @@ -1,8 +1,8 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra/image.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:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/trash.pb.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:fixnum/fixnum.dart' as $fixnum; @@ -10,16 +10,15 @@ import 'package:fixnum/fixnum.dart' as $fixnum; import 'sizes.dart'; class TrashCell extends StatelessWidget { - const TrashCell({ - super.key, - required this.object, - required this.onRestore, - required this.onDelete, - }); - final VoidCallback onRestore; final VoidCallback onDelete; final TrashPB object; + const TrashCell({ + required this.object, + required this.onRestore, + required this.onDelete, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -43,7 +42,7 @@ class TrashCell extends StatelessWidget { width: TrashSizes.actionIconWidth, onPressed: onRestore, iconPadding: const EdgeInsets.all(5), - icon: const FlowySvg(FlowySvgs.restore_s), + icon: const FlowySvg(name: 'editor/restore'), ), const HSpace(20), FlowyIconButton( @@ -51,7 +50,7 @@ class TrashCell extends StatelessWidget { width: TrashSizes.actionIconWidth, onPressed: onDelete, iconPadding: const EdgeInsets.all(5), - icon: const FlowySvg(FlowySvgs.delete_s), + icon: const FlowySvg(name: 'editor/delete'), ), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_header.dart b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_header.dart index 34051428a9..79cdb5d08f 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_header.dart @@ -30,15 +30,13 @@ class TrashHeaderDelegate extends SliverPersistentHeaderDelegate { } class TrashHeaderItem { - TrashHeaderItem({required this.width, required this.title}); - double width; String title; + + TrashHeaderItem({required this.width, required this.title}); } class TrashHeader extends StatelessWidget { - TrashHeader({super.key}); - final List items = [ TrashHeaderItem( title: LocaleKeys.trash_pageHeader_fileName.tr(), @@ -54,6 +52,8 @@ class TrashHeader extends StatelessWidget { ), ]; + TrashHeader({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final headerItems = List.empty(growable: true); diff --git a/frontend/appflowy_flutter/lib/plugins/trash/trash.dart b/frontend/appflowy_flutter/lib/plugins/trash/trash.dart index f3fb4a8bbe..768a660ed2 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash.dart @@ -1,18 +1,16 @@ -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 'trash_page.dart'; - export "./src/sizes.dart"; export "./src/trash_cell.dart"; export "./src/trash_header.dart"; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.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'; + class TrashPluginBuilder extends PluginBuilder { @override Plugin build(dynamic data) { @@ -23,13 +21,10 @@ class TrashPluginBuilder extends PluginBuilder { String get menuName => "TrashPB"; @override - FlowySvgData get icon => FlowySvgs.trash_m; + String get menuIcon => "editor/delete"; @override PluginType get pluginType => PluginType.trash; - - @override - ViewLayoutPB get layoutType => ViewLayoutPB.Document; } class TrashPluginConfig implements PluginConfig { @@ -38,10 +33,10 @@ class TrashPluginConfig implements PluginConfig { } class TrashPlugin extends Plugin { - TrashPlugin({required PluginType pluginType}) : _pluginType = pluginType; - final PluginType _pluginType; + TrashPlugin({required PluginType pluginType}) : _pluginType = pluginType; + @override PluginWidgetBuilder get widgetBuilder => TrashPluginDisplay(); @@ -53,25 +48,16 @@ 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; - @override Widget? get rightBarItem => null; @override - Widget buildWidget({ - required PluginContext context, - required bool shrinkWrap, - Map? data, - }) => - const TrashPage(key: ValueKey('TrashPage')); + Widget buildWidget({PluginContext? context}) => 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..4ade561baa 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.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/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:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; @@ -14,6 +11,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'; @@ -21,7 +19,7 @@ import 'application/trash_bloc.dart'; import 'src/trash_cell.dart'; class TrashPage extends StatefulWidget { - const TrashPage({super.key}); + const TrashPage({Key? key}) : super(key: key); @override State createState() => _TrashPageState(); @@ -29,13 +27,6 @@ class TrashPage extends StatefulWidget { class _TrashPageState extends State { final ScrollController _scrollController = ScrollController(); - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { const horizontalPadding = 80.0; @@ -45,6 +36,7 @@ class _TrashPageState extends State { builder: (context, state) { return SizedBox.expand( child: Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ _renderTopBar(context, state), const VSpace(32), @@ -66,6 +58,7 @@ class _TrashPageState extends State { scrollbarPadding: EdgeInsets.only(top: TrashSizes.headerHeight), barSize: barSize, child: StyledSingleChildScrollView( + controller: ScrollController(), barSize: barSize, axis: Axis.horizontal, child: SizedBox( @@ -102,39 +95,22 @@ class _TrashPageState extends State { const Spacer(), IntrinsicWidth( child: FlowyButton( - text: FlowyText.medium( - LocaleKeys.trash_restoreAll.tr(), - lineHeight: 1.0, - ), - 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()), - ), + text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()), + leftIcon: const FlowySvg(name: 'editor/restore'), + onTap: () => context.read().add( + const TrashEvent.restoreAll(), + ), ), ), const HSpace(6), IntrinsicWidth( child: FlowyButton( - text: FlowyText.medium( - LocaleKeys.trash_deleteAll.tr(), - lineHeight: 1.0, - ), - 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()), - ), + text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()), + leftIcon: const FlowySvg(name: 'editor/delete'), + onTap: () => + context.read().add(const TrashEvent.deleteAll()), ), - ), + ) ], ), ); @@ -157,26 +133,11 @@ 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: () { + context.read().add(TrashEvent.putback(object.id)); + }, + onDelete: () => + context.read().add(TrashEvent.delete(object)), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/util.dart b/frontend/appflowy_flutter/lib/plugins/util.dart index d7ec567abe..75fea12158 100644 --- a/frontend/appflowy_flutter/lib/plugins/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/util.dart @@ -1,28 +1,33 @@ import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; -class ViewPluginNotifier extends PluginNotifier { +class ViewPluginNotifier extends PluginNotifier> { + final ViewListener? _viewListener; + ViewPB view; + + @override + final ValueNotifier> isDeleted = ValueNotifier(none()); + ViewPluginNotifier({ required this.view, }) : _viewListener = ViewListener(viewId: view.id) { _viewListener?.start( - onViewUpdated: (updatedView) => view = updatedView, - onViewMoveToTrash: (result) => result.fold( - (deletedView) => isDeleted.value = deletedView, - (err) => Log.error(err), - ), + onViewUpdated: (updatedView) { + view = updatedView; + }, + onViewMoveToTrash: (result) { + result.fold( + (deletedView) => isDeleted.value = some(deletedView), + (err) => Log.error(err), + ); + }, ); } - ViewPB view; - final ViewListener? _viewListener; - - @override - final ValueNotifier isDeleted = ValueNotifier(null); - @override void dispose() { isDeleted.dispose(); 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_role_pb_extension.dart b/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart deleted file mode 100644 index c20ba0db10..0000000000 --- a/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension AFRolePBExtension on AFRolePB { - bool get isOwner => this == AFRolePB.Owner; - - bool get isMember => this == AFRolePB.Member; - - bool get canInvite => isOwner; - - bool get canDelete => isOwner; - - bool get canUpdate => isOwner; - - bool get canLeave => this != AFRolePB.Owner; - - String get description { - switch (this) { - case AFRolePB.Owner: - return LocaleKeys.settings_appearance_members_owner.tr(); - case AFRolePB.Member: - return LocaleKeys.settings_appearance_members_member.tr(); - case AFRolePB.Guest: - return LocaleKeys.settings_appearance_members_guest.tr(); - } - throw UnimplementedError('Unknown role: $this'); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart b/frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart deleted file mode 100644 index 4036d37b77..0000000000 --- a/frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:path_provider/path_provider.dart'; - -class FlowyCacheManager { - final _caches = []; - - // if you add a new cache, you should register it here. - void registerCache(ICache cache) { - _caches.add(cache); - } - - void unregisterAllCache(ICache cache) { - _caches.clear(); - } - - Future clearAllCache() async { - try { - for (final cache in _caches) { - await cache.clearAll(); - } - - Log.info('Cache cleared'); - } catch (e) { - Log.error(e); - } - } - - Future getCacheSize() async { - try { - int tmpDirSize = 0; - for (final cache in _caches) { - tmpDirSize += await cache.cacheSize(); - } - Log.info('Cache size: $tmpDirSize'); - return tmpDirSize; - } catch (e) { - Log.error(e); - return 0; - } - } -} - -abstract class ICache { - Future cacheSize(); - Future clearAll(); -} - -class TemporaryDirectoryCache implements ICache { - @override - Future cacheSize() async { - final tmpDir = await getTemporaryDirectory(); - final tmpDirStat = await tmpDir.stat(); - return tmpDirStat.size; - } - - @override - Future clearAll() async { - final tmpDir = await getTemporaryDirectory(); - await tmpDir.delete(recursive: true); - } -} - -class FeatureFlagCache implements ICache { - @override - Future cacheSize() async { - return 0; - } - - @override - Future clearAll() async { - await FeatureFlag.clear(); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart deleted file mode 100644 index 090db27ddc..0000000000 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart +++ /dev/null @@ -1,276 +0,0 @@ -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 { - const FlowyNetworkImage({ - super.key, - this.userProfilePB, - this.width, - this.height, - this.fit = BoxFit.cover, - 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 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', - ); - - // clear the cache and retry - await manager.removeFile(widget.url); - _retryLoadImage(); - }, - ); - }, - ); - } - - /// 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() { - final header = {}; - final token = widget.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'); - } - } - 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/conditional_listenable_builder.dart b/frontend/appflowy_flutter/lib/shared/conditional_listenable_builder.dart deleted file mode 100644 index 661e86dcb7..0000000000 --- a/frontend/appflowy_flutter/lib/shared/conditional_listenable_builder.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class ConditionalListenableBuilder extends StatefulWidget { - const ConditionalListenableBuilder({ - super.key, - required this.valueListenable, - required this.buildWhen, - required this.builder, - this.child, - }); - - /// The [ValueListenable] whose value you depend on in order to build. - /// - /// This widget does not ensure that the [ValueListenable]'s value is not - /// null, therefore your [builder] may need to handle null values. - final ValueListenable valueListenable; - - /// The [buildWhen] function will be called on each value change of the - /// [valueListenable]. If the [buildWhen] function returns true, the [builder] - /// will be called with the new value of the [valueListenable]. - /// - final bool Function(T previous, T current) buildWhen; - - /// A [ValueWidgetBuilder] which builds a widget depending on the - /// [valueListenable]'s value. - /// - /// Can incorporate a [valueListenable] value-independent widget subtree - /// from the [child] parameter into the returned widget tree. - final ValueWidgetBuilder builder; - - /// A [valueListenable]-independent widget which is passed back to the [builder]. - /// - /// This argument is optional and can be null if the entire widget subtree the - /// [builder] builds depends on the value of the [valueListenable]. For - /// example, in the case where the [valueListenable] is a [String] and the - /// [builder] returns a [Text] widget with the current [String] value, there - /// would be no useful [child]. - final Widget? child; - - @override - State createState() => - _ConditionalListenableBuilderState(); -} - -class _ConditionalListenableBuilderState - extends State> { - late T value; - - @override - void initState() { - super.initState(); - value = widget.valueListenable.value; - widget.valueListenable.addListener(_valueChanged); - } - - @override - void didUpdateWidget(ConditionalListenableBuilder oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.valueListenable != widget.valueListenable) { - oldWidget.valueListenable.removeListener(_valueChanged); - value = widget.valueListenable.value; - widget.valueListenable.addListener(_valueChanged); - } - } - - @override - void dispose() { - widget.valueListenable.removeListener(_valueChanged); - super.dispose(); - } - - void _valueChanged() { - if (widget.buildWhen(value, widget.valueListenable.value)) { - setState(() { - value = widget.valueListenable.value; - }); - } else { - value = widget.valueListenable.value; - } - } - - @override - Widget build(BuildContext context) { - return widget.builder(context, value, widget.child); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart b/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart deleted file mode 100644 index f2e6d9cc0a..0000000000 --- a/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:appflowy/shared/appflowy_cache_manager.dart'; -import 'package:appflowy/startup/tasks/prelude.dart'; -import 'package:file/file.dart' hide FileSystem; -import 'package:file/local.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:path/path.dart' as p; - -class CustomImageCacheManager extends CacheManager - with ImageCacheManager - implements ICache { - CustomImageCacheManager._() - : super( - Config( - key, - fileSystem: CustomIOFileSystem(key), - ), - ); - - factory CustomImageCacheManager() => _instance; - - static final CustomImageCacheManager _instance = CustomImageCacheManager._(); - - static const key = 'image_cache'; - - @override - Future cacheSize() async { - // https://github.com/Baseflow/flutter_cache_manager/issues/239#issuecomment-719475429 - // this package does not provide a way to get the cache size - return 0; - } - - @override - Future clearAll() async { - await emptyCache(); - } -} - -class CustomIOFileSystem implements FileSystem { - CustomIOFileSystem(this._cacheKey) : _fileDir = createDirectory(_cacheKey); - final Future _fileDir; - final String _cacheKey; - - static Future createDirectory(String key) async { - final baseDir = await appFlowyApplicationDataDirectory(); - final path = p.join(baseDir.path, key); - - const fs = LocalFileSystem(); - final directory = fs.directory(path); - await directory.create(recursive: true); - return directory; - } - - @override - Future createFile(String name) async { - final directory = await _fileDir; - if (!(await directory.exists())) { - await createDirectory(_cacheKey); - } - return directory.childFile(name); - } -} 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/error_page/error_page.dart b/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart deleted file mode 100644 index 9661fd822a..0000000000 --- a/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart +++ /dev/null @@ -1,272 +0,0 @@ -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'; - -class FlowyErrorPage extends StatelessWidget { - factory FlowyErrorPage.error( - Error e, { - required String howToFix, - Key? key, - List? actions, - }) => - FlowyErrorPage._( - e.toString(), - stackTrace: e.stackTrace?.toString(), - howToFix: howToFix, - key: key, - actions: actions, - ); - - factory FlowyErrorPage.message( - String message, { - required String howToFix, - String? stackTrace, - Key? key, - List? actions, - }) => - FlowyErrorPage._( - message, - key: key, - stackTrace: stackTrace, - howToFix: howToFix, - actions: actions, - ); - - factory FlowyErrorPage.exception( - Exception e, { - required String howToFix, - String? stackTrace, - Key? key, - List? actions, - }) => - FlowyErrorPage._( - e.toString(), - stackTrace: stackTrace, - key: key, - howToFix: howToFix, - actions: actions, - ); - - const FlowyErrorPage._( - this.message, { - required this.howToFix, - this.stackTrace, - super.key, - this.actions, - }); - - static const _titleFontSize = 24.0; - static const _titleToMessagePadding = 8.0; - - final List? actions; - final String howToFix; - final String message; - final String? stackTrace; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const FlowyText.medium( - "AppFlowy Error", - fontSize: _titleFontSize, - ), - const SizedBox(height: _titleToMessagePadding), - Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (_) async { - await getIt().setData( - ClipboardServiceData(plainText: message), - ); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, - content: FlowyText( - 'Message copied to clipboard', - fontSize: kIsWeb || !Platform.isIOS && !Platform.isAndroid - ? 14 - : 12, - ), - ), - ); - } - }, - child: FlowyHover( - style: HoverStyle( - backgroundColor: - Theme.of(context).colorScheme.tertiaryContainer, - ), - cursor: SystemMouseCursors.click, - child: FlowyTooltip( - message: 'Click to copy message', - child: Padding( - padding: const EdgeInsets.all(4), - child: FlowyText.semibold(message, maxLines: 10), - ), - ), - ), - ), - const SizedBox(height: _titleToMessagePadding), - FlowyText.regular(howToFix, maxLines: 10), - const SizedBox(height: _titleToMessagePadding), - GitHubRedirectButton( - title: 'Unexpected error', - message: message, - stackTrace: stackTrace, - ), - const SizedBox(height: _titleToMessagePadding), - if (stackTrace != null) StackTracePreview(stackTrace!), - if (actions != null) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: actions!, - ), - ], - ), - ); - } -} - -class StackTracePreview extends StatelessWidget { - const StackTracePreview( - this.stackTrace, { - super.key, - }); - - final String stackTrace; - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 350, - maxWidth: 450, - ), - child: Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.antiAlias, - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - const Align( - alignment: Alignment.centerLeft, - child: FlowyText.semibold( - "Stack Trace", - ), - ), - Container( - height: 120, - padding: const EdgeInsets.symmetric(vertical: 8), - child: SingleChildScrollView( - child: Text( - stackTrace, - style: Theme.of(context).textTheme.bodySmall, - ), - ), - ), - Align( - alignment: Alignment.centerRight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).onBackground, - text: const FlowyText( - "Copy", - ), - useIntrinsicWidth: true, - onTap: () => getIt().setData( - ClipboardServiceData(plainText: stackTrace), - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class GitHubRedirectButton extends StatelessWidget { - const GitHubRedirectButton({ - super.key, - this.title, - this.message, - this.stackTrace, - }); - - final String? title; - final String? message; - final String? stackTrace; - - static const _height = 32.0; - - Uri get _gitHubNewBugUri => Uri( - scheme: 'https', - host: 'github.com', - path: '/AppFlowy-IO/AppFlowy/issues/new', - query: - 'assignees=&labels=&projects=&template=bug_report.yaml&os=$_platform&title=%5BBug%5D+$title&context=$_contextString', - ); - - String get _contextString { - if (message == null && stackTrace == null) { - return ''; - } - - String msg = ""; - if (message != null) { - msg += 'Error message:%0A```%0A$message%0A```%0A'; - } - - if (stackTrace != null) { - msg += 'StackTrace:%0A```%0A$stackTrace%0A```%0A'; - } - - return msg; - } - - String get _platform { - if (kIsWeb) { - return 'Web'; - } - - return Platform.operatingSystem; - } - - @override - Widget build(BuildContext context) { - return FlowyButton( - leftIconSize: const Size.square(_height), - text: FlowyText(LocaleKeys.appName.tr()), - useIntrinsicWidth: true, - leftIcon: const Padding( - padding: EdgeInsets.all(4.0), - child: FlowySvg(FlowySvgData('login/github-mark')), - ), - onTap: () async { - await afLaunchUri(_gitHubNewBugUri); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart deleted file mode 100644 index 7ea66076df..0000000000 --- a/frontend/appflowy_flutter/lib/shared/feature_flags.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:collection/collection.dart'; - -typedef FeatureFlagMap = Map; - -/// The [FeatureFlag] is used to control the front-end features of the app. -/// -/// For example, if your feature is still under development, -/// you can set the value to `false` to hide the feature. -enum FeatureFlag { - // used to control the visibility of the collaborative workspace feature - // if it's on, you can see the workspace list and the workspace settings - // in the top-left corner of the app - collaborativeWorkspace, - - // used to control the visibility of the members settings - // if it's on, you can see the members settings in the settings page - membersSettings, - - // used to control the sync feature of the document - // if it's on, the document will be synced the events from server in real-time - syncDocument, - - // used to control the sync feature of the database - // if it's on, the collaborators will show in the database - syncDatabase, - - // used for the search feature - search, - - // 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; - - static Future initialize() async { - final values = await getIt().getWithFormat( - KVKeys.featureFlag, - (value) => Map.from(jsonDecode(value)).map( - (key, value) { - final k = FeatureFlag.values.firstWhereOrNull( - (e) => e.name == key, - ) ?? - FeatureFlag.unknown; - return MapEntry(k, value as bool); - }, - ), - ) ?? - {}; - - _values = { - ...{for (final flag in FeatureFlag.values) flag: false}, - ...values, - }; - } - - static UnmodifiableMapView get data => - UnmodifiableMapView(_values); - - Future turnOn() async { - await update(true); - } - - Future turnOff() async { - await update(false); - } - - Future update(bool value) async { - _values[this] = value; - - await getIt().set( - KVKeys.featureFlag, - jsonEncode( - _values.map((key, value) => MapEntry(key.name, value)), - ), - ); - } - - static Future clear() async { - _values = {}; - await getIt().remove(KVKeys.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 - FeatureFlag.collaborativeWorkspace, - FeatureFlag.membersSettings, - // release this feature in version 0.5.4 - FeatureFlag.syncDatabase, - FeatureFlag.syncDocument, - FeatureFlag.inlineSubPageMention, - ].contains(this)) { - return true; - } - - if (_values.containsKey(this)) { - return _values[this]!; - } - - switch (this) { - case FeatureFlag.planBilling: - 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; - } - } - - String get description { - switch (this) { - case FeatureFlag.collaborativeWorkspace: - return 'if it\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app'; - case FeatureFlag.membersSettings: - return 'if it\'s on, you can see the members settings in the settings page'; - case FeatureFlag.syncDocument: - return 'if it\'s on, the document will be synced in real-time'; - case FeatureFlag.syncDatabase: - return 'if it\'s on, the collaborators will show in the database'; - case FeatureFlag.search: - 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 ''; - } - } - - String get key => 'appflowy_feature_flag_${toString()}'; -} - -FeatureFlagMap _values = {}; diff --git a/frontend/appflowy_flutter/lib/shared/feedback_gesture_detector.dart b/frontend/appflowy_flutter/lib/shared/feedback_gesture_detector.dart deleted file mode 100644 index 0d482fb985..0000000000 --- a/frontend/appflowy_flutter/lib/shared/feedback_gesture_detector.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -enum HapticFeedbackType { - light, - medium, - heavy, - selection, - vibrate; - - void call() { - switch (this) { - case HapticFeedbackType.light: - HapticFeedback.lightImpact(); - break; - case HapticFeedbackType.medium: - HapticFeedback.mediumImpact(); - break; - case HapticFeedbackType.heavy: - HapticFeedback.heavyImpact(); - break; - case HapticFeedbackType.selection: - HapticFeedback.selectionClick(); - break; - case HapticFeedbackType.vibrate: - HapticFeedback.vibrate(); - break; - } - } -} - -class FeedbackGestureDetector extends GestureDetector { - FeedbackGestureDetector({ - super.key, - HitTestBehavior behavior = HitTestBehavior.opaque, - HapticFeedbackType feedbackType = HapticFeedbackType.light, - required Widget child, - required VoidCallback onTap, - }) : super( - behavior: behavior, - onTap: () { - feedbackType.call(); - onTap(); - }, - child: child, - ); -} 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/flowy_gradient_colors.dart b/frontend/appflowy_flutter/lib/shared/flowy_gradient_colors.dart deleted file mode 100644 index 836f3b37cb..0000000000 --- a/frontend/appflowy_flutter/lib/shared/flowy_gradient_colors.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; - -enum FlowyGradientColor { - gradient1, - gradient2, - gradient3, - gradient4, - gradient5, - gradient6, - gradient7; - - static FlowyGradientColor fromId(String id) { - return FlowyGradientColor.values.firstWhere( - (element) => element.id == id, - orElse: () => FlowyGradientColor.gradient1, - ); - } - - String get id { - // DON'T change this name because it's saved in the database! - switch (this) { - case FlowyGradientColor.gradient1: - return 'appflowy_them_color_gradient1'; - case FlowyGradientColor.gradient2: - return 'appflowy_them_color_gradient2'; - case FlowyGradientColor.gradient3: - return 'appflowy_them_color_gradient3'; - case FlowyGradientColor.gradient4: - return 'appflowy_them_color_gradient4'; - case FlowyGradientColor.gradient5: - return 'appflowy_them_color_gradient5'; - case FlowyGradientColor.gradient6: - return 'appflowy_them_color_gradient6'; - case FlowyGradientColor.gradient7: - return 'appflowy_them_color_gradient7'; - } - } - - LinearGradient get linear { - switch (this) { - case FlowyGradientColor.gradient1: - return const LinearGradient( - begin: Alignment(-0.35, -0.94), - end: Alignment(0.35, 0.94), - colors: [Color(0xFF34BDAF), Color(0xFFB682D4)], - ); - case FlowyGradientColor.gradient2: - return const LinearGradient( - begin: Alignment(0.00, -1.00), - end: Alignment(0, 1), - colors: [Color(0xFF4CC2CC), Color(0xFFE17570)], - ); - case FlowyGradientColor.gradient3: - return const LinearGradient( - begin: Alignment(0.00, -1.00), - end: Alignment(0, 1), - colors: [Color(0xFFAF70E0), Color(0xFFED7196)], - ); - case FlowyGradientColor.gradient4: - return const LinearGradient( - begin: Alignment(0.00, -1.00), - end: Alignment(0, 1), - colors: [Color(0xFFA348D6), Color(0xFF44A7DE)], - ); - case FlowyGradientColor.gradient5: - return const LinearGradient( - begin: Alignment(0.38, -0.93), - end: Alignment(-0.38, 0.93), - colors: [Color(0xFF5749C9), Color(0xFFBB4997)], - ); - case FlowyGradientColor.gradient6: - return const LinearGradient( - begin: Alignment(0.00, -1.00), - end: Alignment(0, 1), - colors: [Color(0xFF036FFA), Color(0xFF00B8E5)], - ); - case FlowyGradientColor.gradient7: - return const LinearGradient( - begin: Alignment(0.62, -0.79), - end: Alignment(-0.62, 0.79), - colors: [Color(0xFFF0C6CF), Color(0xFFDECCE2), Color(0xFFCAD3F9)], - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart b/frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart deleted file mode 100644 index 3e6a69153a..0000000000 --- a/frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -const _defaultFontFamilies = [ - defaultFontFamily, - builtInCodeFontFamily, -]; - -// if the font family is not available, google fonts packages will throw an exception -// this method will return the system font family if the font family is not available -TextStyle getGoogleFontSafely( - String fontFamily, { - FontWeight? fontWeight, - double? fontSize, - Color? fontColor, - double? letterSpacing, - double? lineHeight, -}) { - // if the font family is the built-in font family, we can use it directly - if (_defaultFontFamilies.contains(fontFamily)) { - return TextStyle( - fontFamily: fontFamily.isEmpty ? null : fontFamily, - fontWeight: fontWeight, - fontSize: fontSize, - color: fontColor, - letterSpacing: letterSpacing, - height: lineHeight, - ); - } else { - try { - return GoogleFonts.getFont( - fontFamily, - fontWeight: fontWeight, - fontSize: fontSize, - color: fontColor, - letterSpacing: letterSpacing, - height: lineHeight, - ); - } catch (_) {} - } - - return TextStyle( - fontWeight: fontWeight, - fontSize: fontSize, - color: fontColor, - letterSpacing: letterSpacing, - height: lineHeight, - ); -} 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/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart deleted file mode 100644 index 4520a2b118..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.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/shared/icon_emoji_picker/emoji_skin_tone.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 EmojiKeywordChangedCallback = void Function(String keyword); -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; - final EmojiSelectedCallback onRandomEmojiSelected; - - @override - State createState() => _FlowyEmojiSearchBarState(); -} - -class _FlowyEmojiSearchBarState extends State { - final TextEditingController controller = TextEditingController(); - EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none; - - @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), - _RandomEmojiButton( - skinTone: skinTone, - emojiData: widget.emojiData, - onRandomEmojiSelected: widget.onRandomEmojiSelected, - ), - const HSpace(8.0), - FlowyEmojiSkinToneSelector( - onEmojiSkinToneChanged: (v) { - setState(() { - skinTone = v; - }); - widget.onSkinToneChanged.call(v); - }, - ), - ], - ), - ); - } -} - -class _RandomEmojiButton extends StatelessWidget { - const _RandomEmojiButton({ - required this.skinTone, - required this.emojiData, - required this.onRandomEmojiSelected, - }); - - final EmojiSkinTone skinTone; - final EmojiData emojiData; - final EmojiSelectedCallback onRandomEmojiSelected; - - @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: () { - final random = emojiData.random; - final emojiId = random.$1; - final emoji = emojiData.getEmojiById( - emojiId, - skinTone: skinTone, - ); - onRandomEmojiSelected( - emojiId, - emoji, - ); - }, - ), - ), - ); - } -} - -class _SearchTextField extends StatefulWidget { - const _SearchTextField({ - required this.onKeywordChanged, - this.ensureFocus = false, - }); - - final EmojiKeywordChangedCallback 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/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_skin_tone.dart deleted file mode 100644 index aa97980182..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_skin_tone.dart +++ /dev/null @@ -1,105 +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'; -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; - -@visibleForTesting -ValueKey emojiSkinToneKey(String icon) { - return ValueKey('emoji_skin_tone_$icon'); -} - -class FlowyEmojiSkinToneSelector extends StatefulWidget { - const FlowyEmojiSkinToneSelector({ - super.key, - required this.onEmojiSkinToneChanged, - }); - - final EmojiSkinToneChanged onEmojiSkinToneChanged; - - @override - State createState() => - _FlowyEmojiSkinToneSelectorState(); -} - -class _FlowyEmojiSkinToneSelectorState - extends State { - EmojiSkinTone skinTone = EmojiSkinTone.none; - final controller = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - controller: controller, - popupBuilder: (context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: EmojiSkinTone.values - .map( - (e) => _buildIconButton( - e.icon, - () { - setState(() => lastSelectedEmojiSkinTone = e); - widget.onEmojiSkinToneChanged(e); - controller.close(); - }, - ), - ) - .toList(), - ); - }, - child: FlowyTooltip( - message: LocaleKeys.emoji_selectSkinTone.tr(), - child: _buildIconButton( - lastSelectedEmojiSkinTone?.icon ?? '👋', - () => controller.show(), - ), - ), - ); - } - - Widget _buildIconButton(String icon, VoidCallback onPressed) { - return Container( - width: 36, - height: 36, - decoration: BoxDecoration( - border: Border.all(color: context.pickerButtonBoarderColor), - borderRadius: BorderRadius.circular(8), - ), - child: FlowyButton( - key: emojiSkinToneKey(icon), - margin: EdgeInsets.zero, - text: FlowyText.emoji( - icon, - fontSize: 24.0, - ), - onTap: onPressed, - ), - ); - } -} - -extension EmojiSkinToneIcon on EmojiSkinTone { - String get icon { - switch (this) { - case EmojiSkinTone.none: - return '👋'; - case EmojiSkinTone.light: - return '👋🏻'; - case EmojiSkinTone.mediumLight: - return '👋🏼'; - case EmojiSkinTone.medium: - return '👋🏽'; - case EmojiSkinTone.mediumDark: - return '👋🏾'; - case EmojiSkinTone.dark: - return '👋🏿'; - } - } -} 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/list_extension.dart b/frontend/appflowy_flutter/lib/shared/list_extension.dart deleted file mode 100644 index e701ec3c5e..0000000000 --- a/frontend/appflowy_flutter/lib/shared/list_extension.dart +++ /dev/null @@ -1,8 +0,0 @@ -extension Unique on List { - List unique([Id Function(E element)? id]) { - final ids = {}; - final list = [...this]; - list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id)); - return list; - } -} 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 deleted file mode 100644 index 862f9c4778..0000000000 --- a/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart +++ /dev/null @@ -1,57 +0,0 @@ -const _trailingZerosPattern = r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$'; -final trailingZerosRegex = RegExp(_trailingZerosPattern); - -const _hrefPattern = - r'https?://(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(?:/[^\s]*)?'; -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 -/// -const _imgUrlPattern = - r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.jpeg|.gif|.webm|.webp|.bmp)(\?[^\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: -/// .mp4, .mov, .avi, .webm, .flv, .m4v (mpeg), .mpeg, .h264, -/// -const _videoUrlPattern = - r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.mp4|.mov|.avi|.webm|.flv|.m4v|.mpeg|.h264)(\?[^\s[",><]*)?'; -final videoUrlRegex = RegExp(_videoUrlPattern); - -/// This pattern matches both youtube.com and shortened youtu.be urls. -/// -const _youtubeUrlPattern = r'^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/'; -final youtubeUrlRegex = RegExp(_youtubeUrlPattern); - -const _appflowyCloudUrlPattern = r'^(https:\/\/)(.*)(\.appflowy\.cloud\/)(.*)'; -final appflowyCloudUrlRegex = RegExp(_appflowyCloudUrlPattern); - -const _camelCasePattern = '(?<=[a-z])[A-Z]'; -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/date_time_patterns.dart b/frontend/appflowy_flutter/lib/shared/patterns/date_time_patterns.dart deleted file mode 100644 index 530e9d4558..0000000000 --- a/frontend/appflowy_flutter/lib/shared/patterns/date_time_patterns.dart +++ /dev/null @@ -1,19 +0,0 @@ -/// RegExp to match Twelve Hour formats -/// Source: https://stackoverflow.com/a/33906224 -/// -/// Matches eg: "05:05 PM", "5:50 Pm", "10:59 am", etc. -/// -const _twelveHourTimePattern = - r'\b((1[0-2]|0?[1-9]):([0-5][0-9]) ([AaPp][Mm]))'; -final twelveHourTimeRegex = RegExp(_twelveHourTimePattern); -bool isTwelveHourTime(String? time) => twelveHourTimeRegex.hasMatch(time ?? ''); - -/// RegExp to match Twenty Four Hour formats -/// Source: https://stackoverflow.com/a/7536768 -/// -/// Matches eg: "0:01", "04:59", "16:30", etc. -/// -const _twentyFourHourtimePattern = r'^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'; -final tewentyFourHourTimeRegex = RegExp(_twentyFourHourtimePattern); -bool isTwentyFourHourTime(String? time) => - tewentyFourHourTimeRegex.hasMatch(time ?? ''); 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 deleted file mode 100644 index 4b5ad56ab1..0000000000 --- a/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart +++ /dev/null @@ -1,103 +0,0 @@ -// Check if the user has the required permission to access the device's -// - camera -// - storage -// - ... -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; -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/material.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class PermissionChecker { - static Future checkPhotoPermission(BuildContext context) async { - // check the permission first - final status = await Permission.photos.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_photoPermissionTitle.tr(), - maxLines: 3, - textAlign: TextAlign.center, - ), - content: FlowyText( - LocaleKeys.pageStyle_photoPermissionDescription.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) { - // https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937 - Permission permission = Permission.photos; - if (UniversalPlatform.isAndroid && - ApplicationInfo.androidSDKVersion <= 32) { - permission = Permission.storage; - } - // if the permission is denied, we should request the permission - final newStatus = await permission.request(); - if (newStatus.isDenied) { - return false; - } - } - - 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 deleted file mode 100644 index 4738be78f3..0000000000 --- a/frontend/appflowy_flutter/lib/shared/window_title_bar.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:window_manager/window_manager.dart'; - -class WindowsButtonListener extends WindowListener { - WindowsButtonListener(); - - final ValueNotifier isMaximized = ValueNotifier(false); - - @override - void onWindowMaximize() => isMaximized.value = true; - - @override - void onWindowUnmaximize() => isMaximized.value = false; - - void dispose() => isMaximized.dispose(); -} - -class WindowTitleBar extends StatefulWidget { - const WindowTitleBar({ - super.key, - this.leftChildren = const [], - }); - - final List leftChildren; - - @override - State createState() => _WindowTitleBarState(); -} - -class _WindowTitleBarState extends State { - late final WindowsButtonListener? windowsButtonListener; - bool isMaximized = false; - - @override - void initState() { - super.initState(); - - if (UniversalPlatform.isWindows || UniversalPlatform.isLinux) { - windowsButtonListener = WindowsButtonListener(); - windowManager.addListener(windowsButtonListener!); - windowsButtonListener!.isMaximized.addListener(_isMaximizedChanged); - } else { - windowsButtonListener = null; - } - - windowManager - .isMaximized() - .then((v) => mounted ? setState(() => isMaximized = v) : null); - } - - void _isMaximizedChanged() { - if (mounted) { - setState(() => isMaximized = windowsButtonListener!.isMaximized.value); - } - } - - @override - void dispose() { - if (windowsButtonListener != null) { - windowManager.removeListener(windowsButtonListener!); - windowsButtonListener!.isMaximized.removeListener(_isMaximizedChanged); - windowsButtonListener?.dispose(); - } - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final brightness = Theme.of(context).brightness; - - return Container( - height: 40, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - child: DragToMoveArea( - child: Row( - children: [ - const HSpace(4), - ...widget.leftChildren, - const Spacer(), - WindowCaptionButton.minimize( - brightness: brightness, - onPressed: () => windowManager.minimize(), - ), - if (isMaximized) ...[ - WindowCaptionButton.unmaximize( - brightness: brightness, - onPressed: () => windowManager.unmaximize(), - ), - ] else ...[ - WindowCaptionButton.maximize( - brightness: brightness, - onPressed: () => windowManager.maximize(), - ), - ], - WindowCaptionButton.close( - brightness: brightness, - onPressed: () => windowManager.close(), - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 5a8c0fa651..b7d15b793e 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,117 +1,79 @@ import 'package:appflowy/core/config/kv.dart'; 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/trash/application/prelude.dart'; -import 'package:appflowy/shared/appflowy_cache_manager.dart'; -import 'package:appflowy/shared/custom_image_cache_manager.dart'; -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/plugins/database_view/application/field/field_action_sheet_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/prelude.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/user/application/auth/supabase_auth_service.dart'; import 'package:appflowy/user/application/user_listener.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'; -import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:appflowy/workspace/application/settings/appearance/desktop_appearance.dart'; -import 'package:appflowy/workspace/application/settings/appearance/mobile_appearance.dart'; -import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; -import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/util/file_picker/file_picker_impl.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:appflowy/plugins/document/application/prelude.dart'; +import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:appflowy/workspace/application/user/prelude.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/workspace/prelude.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_backend/protobuf/flowy-user/protobuf.dart'; -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:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/application/menu/prelude.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/user/application/prelude.dart'; +import 'package:appflowy/user/presentation/router.dart'; +import 'package:appflowy/plugins/trash/application/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.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( - GetIt getIt, - IntegrationMode mode, - ) async { - // getIt.registerFactory(() => RustKeyValue()); - getIt.registerFactory(() => DartKeyValue()); + static Future resolve(GetIt getIt) async { + _resolveUserDeps(getIt); - await _resolveCloudDeps(getIt); - _resolveUserDeps(getIt, mode); _resolveHomeDeps(getIt); + _resolveFolderDeps(getIt); - _resolveCommonService(getIt, mode); + + _resolveDocDeps(getIt); + + _resolveGridDeps(getIt); + + _resolveCommonService(getIt); } } -Future _resolveCloudDeps(GetIt getIt) async { - final env = await AppFlowyCloudSharedEnv.fromEnv(); - Log.info("cloud setting: $env"); - getIt.registerFactory(() => env); - - if (isAppFlowyCloudEnabled) { - getIt.registerSingleton( - AppFlowyCloudDeepLink(), - dispose: (obj) async { - await obj.dispose(); - }, - ); - } -} - -void _resolveCommonService( - GetIt getIt, - IntegrationMode mode, -) async { +void _resolveCommonService(GetIt getIt) async { + // getIt.registerFactory(() => RustKeyValue()); + getIt.registerFactory(() => DartKeyValue()); getIt.registerFactory(() => FilePicker()); + getIt.registerFactory(() => ApplicationDataStorage()); - getIt.registerFactory( - () => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(), - ); - - getIt.registerFactory( - () => ClipboardService(), - ); - - // theme - getIt.registerFactory( - () => UniversalPlatform.isMobile ? MobileAppearance() : DesktopAppearance(), - ); - - getIt.registerFactory( - () => FlowyCacheManager() - ..registerCache(TemporaryDirectoryCache()) - ..registerCache(CustomImageCacheManager()) - ..registerCache(FeatureFlagCache()), + getIt.registerFactoryAsync( + () async { + final result = await UserBackendService.getCurrentUserProfile(); + return result.fold( + (l) { + throw Exception('Failed to get user profile: ${l.msg}'); + }, + (r) { + return HttpOpenAIRepository( + client: http.Client(), + apiKey: r.openaiKey, + ); + }, + ); + }, ); } -void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { - switch (currentCloudType()) { - case AuthenticatorType.local: - getIt.registerFactory( - () => BackendAuthService( - AuthTypePB.Local, - ), - ); - break; - case AuthenticatorType.appflowyCloud: - case AuthenticatorType.appflowyCloudSelfHost: - case AuthenticatorType.appflowyCloudDevelop: - getIt.registerFactory(() => AppFlowyCloudAuthService()); - break; - } +void _resolveUserDeps(GetIt getIt) { + // getIt.registerFactory(() => AppFlowyAuthService()); + getIt.registerFactory(() => SupabaseAuthService()); getIt.registerFactory(() => AuthRouter()); @@ -122,14 +84,10 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { () => SignUpBloc(getIt()), ); - getIt.registerFactory(() => SplashRouter()); + getIt.registerFactory(() => SplashRoute()); getIt.registerFactory(() => EditPanelBloc()); getIt.registerFactory(() => SplashBloc()); getIt.registerLazySingleton(() => NetworkListener()); - getIt.registerLazySingleton(() => CachedRecentService()); - getIt.registerLazySingleton( - () => SubscriptionSuccessListenable(), - ); } void _resolveHomeDeps(GetIt getIt) { @@ -141,22 +99,24 @@ void _resolveHomeDeps(GetIt getIt) { (user, _) => UserListener(userProfile: user), ); - // share - getIt.registerFactoryParam( - (view, _) => ShareBloc(view: view), + // + getIt.registerLazySingleton(() => HomeStackManager()); + + getIt.registerFactoryParam( + (user, _) => WelcomeBloc( + userService: UserBackendService(userId: user.id), + userWorkspaceListener: UserWorkspaceListener(userProfile: user), + ), ); - getIt.registerSingleton(ActionNavigationBloc()); - - getIt.registerLazySingleton(() => TabsBloc()); - - getIt.registerSingleton(ReminderBloc()); - - getIt.registerSingleton(RenameViewBloc(PopoverController())); + // share + getIt.registerFactoryParam( + (view, _) => DocShareBloc(view: view), + ); } void _resolveFolderDeps(GetIt getIt) { - // Workspace + //workspace getIt.registerFactoryParam( (user, workspaceId) => WorkspaceListener(user: user, workspaceId: workspaceId), @@ -168,18 +128,49 @@ void _resolveFolderDeps(GetIt getIt) { ), ); - // User + getIt.registerFactoryParam( + (user, _) => MenuUserBloc(user), + ); + + //Settings + getIt.registerFactoryParam( + (user, _) => SettingsDialogBloc(user), + ); + + //User getIt.registerFactoryParam( (user, _) => SettingsUserViewBloc(user), ); - // Trash + // trash getIt.registerLazySingleton(() => TrashService()); getIt.registerLazySingleton(() => TrashListener()); getIt.registerFactory( () => TrashBloc(), ); - - // Favorite - getIt.registerFactory(() => FavoriteBloc()); +} + +void _resolveDocDeps(GetIt getIt) { +// Doc + getIt.registerFactoryParam( + (view, _) => DocumentBloc(view: view), + ); +} + +void _resolveGridDeps(GetIt getIt) { + getIt.registerFactoryParam( + (viewId, fieldController) => GridHeaderBloc( + viewId: viewId, + fieldController: fieldController, + ), + ); + + getIt.registerFactoryParam( + (data, _) => FieldActionSheetBloc(fieldCellContext: data), + ); + + getIt.registerFactoryParam( + (viewId, cache) => + DatabasePropertyBloc(viewId: viewId, fieldController: cache), + ); } diff --git a/frontend/appflowy_flutter/lib/startup/entry_point.dart b/frontend/appflowy_flutter/lib/startup/entry_point.dart index 33eb1bf982..44f2631da2 100644 --- a/frontend/appflowy_flutter/lib/startup/entry_point.dart +++ b/frontend/appflowy_flutter/lib/startup/entry_point.dart @@ -1,11 +1,13 @@ import 'package:appflowy/startup/launch_configuration.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/presentation/screens/splash_screen.dart'; +import 'package:appflowy/user/presentation/splash_screen.dart'; import 'package:flutter/material.dart'; -class AppFlowyApplication implements EntryPoint { +class FlowyApp implements EntryPoint { @override Widget create(LaunchConfiguration config) { - return SplashScreen(isAnon: config.isAnon); + return SplashScreen( + autoRegister: config.autoRegistrationSupported, + ); } } diff --git a/frontend/appflowy_flutter/lib/startup/launch_configuration.dart b/frontend/appflowy_flutter/lib/startup/launch_configuration.dart index da1a64db3e..22d710612a 100644 --- a/frontend/appflowy_flutter/lib/startup/launch_configuration.dart +++ b/frontend/appflowy_flutter/lib/startup/launch_configuration.dart @@ -1,13 +1,8 @@ class LaunchConfiguration { const LaunchConfiguration({ - this.isAnon = false, - required this.version, - required this.rustEnvs, + this.autoRegistrationSupported = false, }); // APP will automatically register after launching. - final bool isAnon; - final String version; - // - final Map rustEnvs; + final bool autoRegistrationSupported; } diff --git a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart index 5bb08e3fdf..28c38f5ead 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart @@ -1,29 +1,25 @@ -library; +library flowy_plugin; -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:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/widgets.dart'; export "./src/sandbox.dart"; enum PluginType { - document, + editor, blank, trash, grid, board, calendar, - databaseDocument, - chat, } typedef PluginId = String; -abstract class Plugin { +abstract class Plugin { PluginId get id; PluginWidgetBuilder get widgetBuilder; @@ -32,8 +28,6 @@ abstract class Plugin { PluginType get pluginType; - void init() {} - void dispose() { notifier?.dispose(); } @@ -51,14 +45,14 @@ abstract class PluginBuilder { String get menuName; - FlowySvgData get icon; + String get menuIcon; /// The type of this [Plugin]. Each [Plugin] should have a unique [PluginType] PluginType get pluginType; /// The layoutType is used in the backend to determine the layout of the view. - /// Currently, AppFlowy supports 4 layout types: Document, Grid, Board, Calendar. - ViewLayoutPB? get layoutType; + /// Currrently, AppFlowy supports 4 layout types: Document, Grid, Board, Calendar. + ViewLayoutPB? get layoutType => ViewLayoutPB.Document; } abstract class PluginConfig { @@ -72,22 +66,14 @@ abstract class PluginWidgetBuilder with NavigationItem { EdgeInsets get contentPadding => const EdgeInsets.symmetric(horizontal: 40, vertical: 28); - Widget buildWidget({ - required PluginContext context, - required bool shrinkWrap, - Map? data, - }); + Widget buildWidget({PluginContext? context}); } class PluginContext { - PluginContext({ - this.userProfile, - this.onDeleted, - }); - // calls when widget of the plugin get deleted - final Function(ViewPB, int?)? onDeleted; - final UserProfilePB? userProfile; + final Function(ViewPB, int?) onDeleted; + + PluginContext({required this.onDeleted}); } void registerPlugin({required PluginBuilder builder, PluginConfig? config}) { diff --git a/frontend/appflowy_flutter/lib/startup/plugin/src/sandbox.dart b/frontend/appflowy_flutter/lib/startup/plugin/src/sandbox.dart index efcc5f9707..a10ff9631a 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/src/sandbox.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/src/sandbox.dart @@ -7,16 +7,16 @@ import '../plugin.dart'; import 'runner.dart'; class PluginSandbox { - PluginSandbox() { - pluginRunner = PluginRunner(); - } - final LinkedHashMap _pluginBuilders = LinkedHashMap(); final Map _pluginConfigs = {}; late PluginRunner pluginRunner; + PluginSandbox() { + pluginRunner = PluginRunner(); + } + int indexOf(PluginType pluginType) { final index = _pluginBuilders.keys.toList().indexWhere((ty) => ty == pluginType); @@ -42,7 +42,10 @@ class PluginSandbox { PluginConfig? config, }) { if (_pluginBuilders.containsKey(pluginType)) { - return; + throw PlatformException( + code: '-1', + message: "$pluginType was registered before", + ); } _pluginBuilders[pluginType] = builder; diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 7a282b3856..32199e6154 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -1,23 +1,15 @@ -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/workspace/application/settings/prelude.dart'; +import 'package:appflowy/env/env.dart'; +import 'package:appflowy/workspace/application/settings/settings_location_cubit.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'; -import 'package:package_info_plus/package_info_plus.dart'; 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; @@ -26,181 +18,90 @@ abstract class EntryPoint { Widget create(LaunchConfiguration config); } -class FlowyRunnerContext { - FlowyRunnerContext({required this.applicationDataDirectory}); - - final Directory applicationDataDirectory; -} - -Future runAppFlowy({bool isAnon = false}) async { - Log.info('restart AppFlowy: isAnon: $isAnon'); - - if (kReleaseMode) { - await FlowyRunner.run( - AppFlowyApplication(), - integrationMode(), - isAnon: isAnon, - ); - } else { - // When running the app in integration test mode, we need to - // specify the mode to run the app again. - await FlowyRunner.run( - AppFlowyApplication(), - FlowyRunner.currentMode, - didInitGetItCallback: IntegrationTestHelper.didInitGetItCallback, - rustEnvsBuilder: IntegrationTestHelper.rustEnvsBuilder, - isAnon: isAnon, - ); - } -} - class FlowyRunner { - // This variable specifies the initial mode of the app when it is launched for the first time. - // The same mode will be automatically applied in subsequent executions when the runAppFlowy() - // method is called. - static var currentMode = integrationMode(); - - static Future run( + static Future run( EntryPoint f, IntegrationMode mode, { - // This callback is triggered after the initialization of 'getIt', - // which is used for dependency injection throughout the app. - // If your functionality depends on 'getIt', ensure to register - // your callback here to execute any necessary actions post-initialization. - Future Function()? didInitGetItCallback, - // Passing the envs to the backend - Map Function()? rustEnvsBuilder, - // Indicate whether the app is running in anonymous mode. - // Note: when the app is running in anonymous mode, the user no need to - // sign in, and the app will only save the data in the local storage. - bool isAnon = false, + LaunchConfiguration config = const LaunchConfiguration( + autoRegistrationSupported: false, + ), }) async { - currentMode = mode; - - // Only set the mode when it's not release mode - if (!kReleaseMode) { - IntegrationTestHelper.didInitGetItCallback = didInitGetItCallback; - IntegrationTestHelper.rustEnvsBuilder = rustEnvsBuilder; - } - - // Clear and dispose tasks from previous AppLaunch - if (getIt.isRegistered(instance: AppLauncher)) { - await getIt().dispose(); - } - // Clear all the states in case of rebuilding. await getIt.reset(); - final config = LaunchConfiguration( - isAnon: isAnon, - // Unit test can't use the package_info_plus plugin - version: mode.isUnitTest - ? '1.0.0' - : await PackageInfo.fromPlatform().then((value) => value.version), - rustEnvs: rustEnvsBuilder?.call() ?? {}, - ); - // Specify the env - await initGetIt(getIt, mode, f, config); - await didInitGetItCallback?.call(); + initGetIt(getIt, mode, f, config); - final applicationDataDirectory = - await getIt().getPath().then( - (value) => Directory(value), - ); + final directory = await getIt() + .getPath() + .then((value) => Directory(value)); + + // final directory = await appFlowyDocumentDirectory(); // add task final launcher = getIt(); launcher.addTasks( [ - // 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(), - // 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 FeatureFlagTask(), - + // handle platform errors. + const PlatformErrorCatcherTask(), // localization const InitLocalizationTask(), // init the app window - InitAppWindowTask(), + const InitAppWindowTask(), // Init Rust SDK - InitRustSDKTask(customApplicationPath: applicationDataDirectory), + InitRustSDKTask(directory: directory), // Load Plugins, like document, grid ... const PluginLoadTask(), - const FileStorageTask(), // init the app widget // ignore in test mode - if (!mode.isUnitTest) ...[ - // 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(), + if (!mode.isTest()) ...[ const HotKeyTask(), - if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(), + InitSupabaseTask( + url: Env.supabaseUrl, + anonKey: Env.supabaseAnonKey, + key: Env.supabaseKey, + jwtSecret: Env.supabaseJwtSecret, + collabTable: Env.supabaseCollabTable, + ), const InitAppWidgetTask(), - const InitPlatformServiceTask(), - const RecentServiceTask(), + const InitPlatformServiceTask() ], ], ); await launcher.launch(); // execute the tasks - - return FlowyRunnerContext( - applicationDataDirectory: applicationDataDirectory, - ); } } Future initGetIt( GetIt getIt, - IntegrationMode mode, + IntegrationMode env, EntryPoint f, LaunchConfiguration config, ) async { getIt.registerFactory(() => f); - getIt.registerLazySingleton( - () { - return FlowySDK(); - }, - dispose: (sdk) async { - await sdk.dispose(); - }, - ); + getIt.registerLazySingleton(() { + return FlowySDK(); + }); getIt.registerLazySingleton( () => AppLauncher( context: LaunchContext( getIt, - mode, + env, config, ), ), - dispose: (launcher) async { - await launcher.dispose(); - }, ); getIt.registerSingleton(PluginSandbox()); - getIt.registerSingleton(ViewExpanderRegistry()); - getIt.registerSingleton(LinkHoverTriggers()); - getIt.registerSingleton( - FloatingToolbarController(), - ); - await DependencyResolver.resolve(getIt, mode); + await DependencyResolver.resolve(getIt); } class LaunchContext { - LaunchContext(this.getIt, this.env, this.config); - GetIt getIt; IntegrationMode env; LaunchConfiguration config; + LaunchContext(this.getIt, this.env, this.config); } enum LaunchTaskType { @@ -216,8 +117,6 @@ abstract class LaunchTask { LaunchTaskType get type => LaunchTaskType.dataProcessing; Future initialize(LaunchContext context); - - Future dispose(); } class AppLauncher { @@ -241,38 +140,28 @@ class AppLauncher { await task.initialize(context); } } - - Future dispose() async { - for (final task in tasks) { - await task.dispose(); - } - tasks.clear(); - } } enum IntegrationMode { develop, release, - unitTest, - integrationTest; - - // test mode - bool get isTest => isUnitTest || isIntegrationTest; - - bool get isUnitTest => this == IntegrationMode.unitTest; - - bool get isIntegrationTest => this == IntegrationMode.integrationTest; - - // release mode - bool get isRelease => this == IntegrationMode.release; - - // develop mode - bool get isDevelop => this == IntegrationMode.develop; + test, + integrationTest, } -IntegrationMode integrationMode() { +extension IntegrationEnvExt on IntegrationMode { + bool isTest() { + return this == IntegrationMode.test; + } + + bool isIntegrationTest() { + return this == IntegrationMode.integrationTest; + } +} + +IntegrationMode integrationEnv() { if (Platform.environment.containsKey('FLUTTER_TEST')) { - return IntegrationMode.unitTest; + return IntegrationMode.test; } if (kReleaseMode) { @@ -281,9 +170,3 @@ IntegrationMode integrationMode() { return IntegrationMode.develop; } - -/// Only used for integration test -class IntegrationTestHelper { - static Future Function()? didInitGetItCallback; - static Map Function()? rustEnvsBuilder; -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 98b76802d4..c08a60dd2c 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -1,38 +1,14 @@ -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/user_settings_service.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/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/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:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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'; +import '../../user/application/user_settings_service.dart'; +import '../../workspace/application/appearance.dart'; +import '../startup.dart'; class InitAppWidgetTask extends LaunchTask { const InitAppWidgetTask(); @@ -42,45 +18,27 @@ class InitAppWidgetTask extends LaunchTask { @override Future initialize(LaunchContext context) async { - WidgetsFlutterBinding.ensureInitialized(); - - await NotificationService.initialize(); - - await loadIconGroups(); - final widget = context.getIt().create(context.config); final appearanceSetting = await UserSettingsBackendService().getAppearanceSetting(); - final dateTimeSettings = - await UserSettingsBackendService().getDateTimeSettings(); - - // If the passed-in context is not the same as the context of the - // application widget, the application widget will be rebuilt. final app = ApplicationWidget( - key: ValueKey(context), appearanceSetting: appearanceSetting, - dateTimeSettings: dateTimeSettings, - appTheme: await appTheme(appearanceSetting.theme), child: widget, ); + Bloc.observer = ApplicationBlocObserver(); runApp( EasyLocalization( supportedLocales: const [ // In alphabetical order - Locale('am', 'ET'), Locale('ar', 'SA'), Locale('ca', 'ES'), - Locale('cs', 'CZ'), - Locale('ckb', 'KU'), Locale('de', 'DE'), Locale('en'), Locale('es', 'VE'), Locale('eu', 'ES'), - Locale('el', 'GR'), Locale('fr', 'FR'), Locale('fr', 'CA'), - Locale('he'), Locale('hu', 'HU'), Locale('id', 'ID'), Locale('it', 'IT'), @@ -89,220 +47,82 @@ class InitAppWidgetTask extends LaunchTask { Locale('pl', 'PL'), Locale('pt', 'BR'), Locale('ru', 'RU'), - Locale('sv', 'SE'), - Locale('th', 'TH'), + Locale('sv'), Locale('tr', 'TR'), - Locale('uk', 'UA'), - Locale('ur'), - Locale('vi', 'VN'), Locale('zh', 'CN'), Locale('zh', 'TW'), - Locale('fa'), - Locale('hin'), - Locale('mr', 'IN'), ], path: 'assets/translations', fallbackLocale: const Locale('en'), useFallbackTranslations: true, + saveLocale: false, child: app, ), ); - return; + return Future(() => {}); } - - @override - Future dispose() async {} } -class ApplicationWidget extends StatefulWidget { - const ApplicationWidget({ - super.key, - required this.child, - required this.appTheme, - required this.appearanceSetting, - required this.dateTimeSettings, - }); - +class ApplicationWidget extends StatelessWidget { final Widget child; - final AppTheme appTheme; final AppearanceSettingsPB appearanceSetting; - final DateTimeSettingsPB dateTimeSettings; - @override - State createState() => _ApplicationWidgetState(); -} - -class _ApplicationWidgetState extends State { - late final GoRouter routerConfig; - - final _commandPaletteNotifier = ValueNotifier(false); - - @override - void initState() { - super.initState(); - // Avoid rebuild routerConfig when the appTheme is changed. - routerConfig = generateRouter(widget.child); - } - - @override - void dispose() { - _commandPaletteNotifier.dispose(); - super.dispose(); - } + const ApplicationWidget({ + Key? key, + required this.child, + required this.appearanceSetting, + }) : super(key: key); @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - if (FeatureFlag.search.isOn) - BlocProvider(create: (_) => CommandPaletteBloc()), - BlocProvider( - create: (_) => AppearanceSettingsCubit( - widget.appearanceSetting, - widget.dateTimeSettings, - widget.appTheme, - )..readLocaleWhenAppLaunch(context), - ), - BlocProvider( - create: (_) => NotificationSettingsCubit(), - ), - BlocProvider( - create: (_) => DocumentAppearanceCubit()..fetch(), - ), - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), - ], - child: BlocListener( - listenWhen: (_, curr) => curr.action != null, - listener: (context, state) { - final action = state.action; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (action?.type == ActionType.openView && - UniversalPlatform.isDesktop) { - final view = - action!.arguments?[ActionArgumentKeys.view] as ViewPB?; - 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, - }, - ); - } - } else if (action?.type == ActionType.openRow && - UniversalPlatform.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, - }, - ); - } - } - }); - }, - 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 themeBuilder = AppFlowyDefaultTheme(); - final brightness = Theme.of(context).brightness; + final cubit = AppearanceSettingsCubit(appearanceSetting) + ..readLocaleWhenAppLaunch(context); - return AnimatedAppFlowyTheme( - data: brightness == Brightness.light - ? themeBuilder.light() - : themeBuilder.dark(), - 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 BlocProvider( + create: (context) => cubit, + child: BlocBuilder( + builder: (context, state) => MaterialApp( + builder: overlayManagerBuilder(), + debugShowCheckedModeBanner: false, + theme: state.lightTheme, + darkTheme: state.darkTheme, + themeMode: state.themeMode, + localizationsDelegates: context.localizationDelegates + + [AppFlowyEditorLocalizations.delegate], + supportedLocales: context.supportedLocales, + locale: state.locale, + navigatorKey: AppGlobals.rootNavKey, + home: child, ), ), ); } - - void _setSystemOverlayStyle(AppearanceSettingsState state) { - if (Platform.isAndroid) { - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.edgeToEdge, - overlays: [], - ); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - systemNavigationBarColor: Colors.transparent, - ), - ); - } - } } class AppGlobals { static GlobalKey rootNavKey = GlobalKey(); - static NavigatorState get nav => rootNavKey.currentState!; - - static BuildContext get context => rootNavKey.currentContext!; } -Future appTheme(String themeName) async { - if (themeName.isEmpty) { - return AppTheme.fallback; - } else { - try { - return await AppTheme.fromName(themeName); - } catch (e) { - return AppTheme.fallback; - } +class ApplicationBlocObserver extends BlocObserver { + @override + // ignore: unnecessary_overrides + void onTransition(Bloc bloc, Transition transition) { + // Log.debug("[current]: ${transition.currentState} \n\n[next]: ${transition.nextState}"); + // Log.debug("${transition.nextState}"); + super.onTransition(bloc, transition); } + + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + Log.debug(error); + super.onError(bloc, error, stackTrace); + } + + // @override + // void onEvent(Bloc bloc, Object? event) { + // Log.debug("$event"); + // super.onEvent(bloc, event); + // } } 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..59c3cb4e59 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 @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'dart:ui'; import 'package:appflowy/core/config/kv.dart'; @@ -6,29 +7,16 @@ 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; - // 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; + static const double minWindowHeight = 400.0; + static const double minWindowWidth = 800.0; static const width = 'width'; static const height = 'height'; - static const String dx = 'dx'; - static const String dy = 'dy'; - - Future setSize(Size size) async { + Future saveSize(Size size) async { final windowSize = { - height: size.height.clamp(minWindowHeight, maxWindowHeight), - width: size.width.clamp(minWindowWidth, maxWindowWidth), + height: max(size.height, minWindowHeight), + width: max(size.width, minWindowWidth), }; await getIt().set( @@ -38,71 +26,11 @@ class WindowSizeManager { } Future getSize() async { - final defaultWindowSize = jsonEncode( - { - WindowSizeManager.height: defaultWindowHeight, - WindowSizeManager.width: defaultWindowWidth, - }, - ); + final defaultWindowSize = jsonEncode({height: 600.0, width: 800.0}); final windowSize = await getIt().get(KVKeys.windowSize); final size = json.decode( - windowSize ?? defaultWindowSize, + windowSize.getOrElse(() => defaultWindowSize), ); - final double width = size[WindowSizeManager.width] ?? minWindowWidth; - final double height = size[WindowSizeManager.height] ?? minWindowHeight; - return Size( - width.clamp(minWindowWidth, maxWindowWidth), - height.clamp(minWindowHeight, maxWindowHeight), - ); - } - - Future setPosition(Offset offset) async { - await getIt().set( - KVKeys.windowPosition, - jsonEncode({ - dx: offset.dx, - dy: offset.dy, - }), - ); - } - - Future getPosition() async { - final position = await getIt().get(KVKeys.windowPosition); - if (position == null) { - return null; - } - final offset = json.decode(position); - return Offset(offset[dx], offset[dy]); - } - - Future getScaleFactor() async { - final scaleFactor = await getIt().getWithFormat( - KVKeys.scaleFactor, - (value) => double.tryParse(value) ?? 1.0, - ) ?? - 1.0; - return scaleFactor.clamp(minScaleFactor, maxScaleFactor); - } - - Future setScaleFactor(double scaleFactor) async { - await getIt().set( - KVKeys.scaleFactor, - '${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; + return Size(size[width]!, size[height]!); } } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart deleted file mode 100644 index 362b27a85a..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -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/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); - } - } - - ValueNotifier? _stateNotifier = ValueNotifier(null); - - Completer>? _completer; - - set completer(Completer>? value) { - Log.debug('AppFlowyCloudDeepLink: $hashCode completer'); - _completer = value; - } - - late final StreamSubscription _deepLinkSubscription; - - Future dispose() async { - Log.debug('AppFlowyCloudDeepLink: $hashCode dispose'); - await _deepLinkSubscription.cancel(); - - _stateNotifier?.dispose(); - _stateNotifier = null; - completer = null; - } - - void registerCompleter( - Completer> completer, - ) { - this.completer = completer; - } - - VoidCallback subscribeDeepLinkLoadingState( - ValueChanged listener, - ) { - void listenerFn() { - if (_stateNotifier?.value != null) { - listener(_stateNotifier!.value!); - } - } - - _stateNotifier?.addListener(listenerFn); - return listenerFn; - } - - void unsubscribeDeepLinkLoadingState(VoidCallback listener) => - _stateNotifier?.removeListener(listener); - - Future passGotrueTokenResponse( - GotrueTokenResponsePB gotrueTokenResponse, - ) async { - final uri = _buildDeepLinkUri(gotrueTokenResponse); - await _handleUri(uri); - } - - Future _handleUri( - Uri? uri, - ) async { - _stateNotifier?.value = DeepLinkResult(state: DeepLinkState.none); - - if (uri == null) { - Log.error('onDeepLinkError: Unexpected empty deep link callback'); - _completer?.complete(FlowyResult.failure(AuthError.emptyDeepLink)); - completer = null; - return; - } - - if (_isPaymentSuccessUri(uri)) { - Log.debug("Payment success deep link: ${uri.toString()}"); - final plan = uri.queryParameters['plan']; - return getIt().onPaymentSuccess(plan); - } - - return _isAuthCallbackDeepLink(uri).fold( - (_) async { - final deviceId = await getDeviceId(); - final payload = OauthSignInPB( - authenticator: AuthTypePB.Server, - map: { - AuthServiceMapKeys.signInURL: uri.toString(), - AuthServiceMapKeys.deviceId: deviceId, - }, - ); - _stateNotifier?.value = DeepLinkResult(state: DeepLinkState.loading); - final result = await UserEventOauthSignIn(payload).send(); - - _stateNotifier?.value = DeepLinkResult( - state: DeepLinkState.finish, - result: result, - ); - // If there is no completer, runAppFlowy() will be called. - if (_completer == null) { - await result.fold( - (_) async { - await runAppFlowy(); - }, - (err) { - Log.error(err); - final context = AppGlobals.rootNavKey.currentState?.context; - if (context != null) { - showToastNotification( - message: err.msg, - ); - } - }, - ); - } else { - _completer?.complete(result); - completer = null; - } - }, - (err) { - Log.error('onDeepLinkError: Unexpected deep link: $err'); - if (_completer == null) { - final context = AppGlobals.rootNavKey.currentState?.context; - if (context != null) { - showSnackBarMessage( - context, - err.msg, - ); - } - } else { - _completer?.complete(FlowyResult.failure(err)); - completer = null; - } - }, - ); - } - - FlowyResult _isAuthCallbackDeepLink(Uri uri) { - if (uri.fragment.contains('access_token')) { - return FlowyResult.success(null); - } - - return FlowyResult.failure( - FlowyError.create() - ..code = ErrorCode.MissingAuthField - ..msg = uri.path, - ); - } - - 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 { - UserAuthStateListener? _authStateListener; - bool isLoggingOut = false; - - @override - Future initialize(LaunchContext context) async { - if (!isAppFlowyCloudEnabled) { - return; - } - _authStateListener = UserAuthStateListener(); - - _authStateListener?.start( - didSignIn: () { - isLoggingOut = false; - }, - onInvalidAuth: (message) async { - Log.error(message); - if (!isLoggingOut) { - await runAppFlowy(); - } - }, - ); - } - - @override - Future dispose() async { - await _authStateListener?.stop(); - _authStateListener = null; - } -} - -class DeepLinkResult { - DeepLinkResult({ - required this.state, - this.result, - }); - - final DeepLinkState state; - final FlowyResult? result; -} - -enum DeepLinkState { - none, - 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 deleted file mode 100644 index 9a34e84f70..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/startup/startup.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'; - -class DebugTask extends LaunchTask { - DebugTask(); - - final Talker talker = Talker(); - - @override - Future initialize(LaunchContext context) async { - // hide the keyboard on mobile - if (UniversalPlatform.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 - Future dispose() async {} -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart deleted file mode 100644 index 2c90afbdda..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart +++ /dev/null @@ -1,126 +0,0 @@ -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'; - -class ApplicationInfo { - static int androidSDKVersion = -1; - 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 { - const ApplicationInfoTask(); - - @override - Future initialize(LaunchContext context) async { - 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; - - 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 - Future dispose() async {} -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/feature_flag_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/feature_flag_task.dart deleted file mode 100644 index fd2439c892..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/feature_flag_task.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:flutter/foundation.dart'; - -import '../startup.dart'; - -class FeatureFlagTask extends LaunchTask { - const FeatureFlagTask(); - - @override - Future initialize(LaunchContext context) async { - // the hotkey manager is not supported on mobile - if (!kDebugMode) { - return; - } - - await FeatureFlag.initialize(); - } - - @override - Future dispose() async {} -} 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 deleted file mode 100644 index e64e0f98de..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ /dev/null @@ -1,696 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart'; -import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart'; -import 'package:appflowy/mobile/presentation/database/card/card.dart'; -import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; -import 'package:appflowy/mobile/presentation/database/field/mobile_create_field_screen.dart'; -import 'package:appflowy/mobile/presentation/database/field/mobile_edit_field_screen.dart'; -import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_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/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/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'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_screen.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/app_widget.dart'; -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: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( - navigatorKey: AppGlobals.rootNavKey, - initialLocation: '/', - routes: [ - // Root route is SplashScreen. - // It needs LaunchConfiguration as a parameter, so we get it from ApplicationWidget's child. - _rootRoute(child), - // Routes in both desktop and mobile - _signInScreenRoute(), - _skipLogInScreenRoute(), - _workspaceErrorScreenRoute(), - // Desktop only - if (UniversalPlatform.isDesktop) _desktopHomeScreenRoute(), - // Mobile only - if (UniversalPlatform.isMobile) ...[ - // settings - _mobileHomeSettingPageRoute(), - _mobileCloudSettingAppFlowyCloudPageRoute(), - _mobileLaunchSettingsPageRoute(), - _mobileFeatureFlagPageRoute(), - - // view page - _mobileEditorScreenRoute(), - _mobileGridScreenRoute(), - _mobileBoardScreenRoute(), - _mobileCalendarScreenRoute(), - _mobileChatScreenRoute(), - // card detail page - _mobileCardDetailScreenRoute(), - _mobileDateCellEditScreenRoute(), - _mobileNewPropertyPageRoute(), - _mobileEditPropertyPageRoute(), - - // home - // MobileHomeSettingPage is outside the bottom navigation bar, thus it is not in the StatefulShellRoute. - _mobileHomeScreenWithNavigationBarRoute(), - - // trash - _mobileHomeTrashPageRoute(), - - // emoji picker - _mobileEmojiPickerPageRoute(), - _mobileImagePickerPageRoute(), - - // color picker - _mobileColorPickerPageRoute(), - - // code language picker - _mobileCodeLanguagePickerPageRoute(), - _mobileLanguagePickerPageRoute(), - _mobileFontPickerPageRoute(), - - // calendar related - _mobileCalendarEventsPageRoute(), - - _mobileBlockSettingsPageRoute(), - - // notifications - _mobileNotificationMultiSelectPageRoute(), - - // invite members - _mobileInviteMembersPageRoute(), - ], - - // Desktop and Mobile - GoRoute( - path: WorkspaceStartScreen.routeName, - pageBuilder: (context, state) { - final args = state.extra as Map; - return CustomTransitionPage( - child: WorkspaceStartScreen( - userProfile: args[WorkspaceStartScreen.argUserProfile], - ), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ), - ], - ); -} - -/// We use StatefulShellRoute to create a StatefulNavigationShell(ScaffoldWithNavBar) to access to multiple pages, and each page retains its own state. -StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() { - return StatefulShellRoute.indexedStack( - builder: ( - BuildContext context, - GoRouterState state, - StatefulNavigationShell navigationShell, - ) { - // Return the widget that implements the custom shell (in this case - // using a BottomNavigationBar). The StatefulNavigationShell is passed - // to be able access the state of the shell and to navigate to other - // branches in a stateful way. - return MobileBottomNavigationBar(navigationShell: navigationShell); - }, - branches: [ - StatefulShellBranch( - routes: [ - GoRoute( - path: MobileHomeScreen.routeName, - builder: (BuildContext context, GoRouterState state) { - return const MobileHomeScreen(); - }, - ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: MobileFavoriteScreen.routeName, - builder: (BuildContext context, GoRouterState state) { - return const MobileFavoriteScreen(); - }, - ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: MobileNotificationsScreenV2.routeName, - builder: (_, __) => const MobileNotificationsScreenV2(), - ), - ], - ), - ], - ); -} - -GoRoute _mobileHomeSettingPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileHomeSettingPage.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage(child: MobileHomeSettingPage()); - }, - ); -} - -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, - path: AppFlowyCloudPage.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage(child: AppFlowyCloudPage()); - }, - ); -} - -GoRoute _mobileLaunchSettingsPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileLaunchSettingsPage.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage(child: MobileLaunchSettingsPage()); - }, - ); -} - -GoRoute _mobileFeatureFlagPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: FeatureFlagScreen.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage(child: FeatureFlagScreen()); - }, - ); -} - -GoRoute _mobileHomeTrashPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileHomeTrashPage.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage(child: MobileHomeTrashPage()); - }, - ); -} - -GoRoute _mobileBlockSettingsPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileBlockSettingsScreen.routeName, - pageBuilder: (context, state) { - final actionsString = - state.uri.queryParameters[MobileBlockSettingsScreen.supportedActions]; - final actions = actionsString - ?.split(',') - .map(MobileBlockActionType.fromActionString) - .toList(); - return MaterialExtendedPage( - child: MobileBlockSettingsScreen( - actions: actions ?? MobileBlockActionType.standard, - ), - ); - }, - ); -} - -GoRoute _mobileEmojiPickerPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileEmojiPickerScreen.routeName, - 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, - ), - ); - }, - ); -} - -GoRoute _mobileColorPickerPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileColorPickerScreen.routeName, - pageBuilder: (context, state) { - final title = - state.uri.queryParameters[MobileColorPickerScreen.pageTitle] ?? ''; - return MaterialExtendedPage( - child: MobileColorPickerScreen( - title: title, - ), - ); - }, - ); -} - -GoRoute _mobileImagePickerPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileImagePickerScreen.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage( - child: MobileImagePickerScreen(), - ); - }, - ); -} - -GoRoute _mobileCodeLanguagePickerPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileCodeLanguagePickerScreen.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage( - child: MobileCodeLanguagePickerScreen(), - ); - }, - ); -} - -GoRoute _mobileLanguagePickerPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: LanguagePickerScreen.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage( - child: LanguagePickerScreen(), - ); - }, - ); -} - -GoRoute _mobileFontPickerPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: FontPickerScreen.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage( - child: FontPickerScreen(), - ); - }, - ); -} - -GoRoute _mobileNewPropertyPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileNewPropertyScreen.routeName, - pageBuilder: (context, state) { - final viewId = state - .uri.queryParameters[MobileNewPropertyScreen.argViewId] as String; - final fieldTypeId = - state.uri.queryParameters[MobileNewPropertyScreen.argFieldTypeId] ?? - FieldType.RichText.value.toString(); - final value = int.parse(fieldTypeId); - return MaterialExtendedPage( - fullscreenDialog: true, - child: MobileNewPropertyScreen( - viewId: viewId, - fieldType: FieldType.valueOf(value), - ), - ); - }, - ); -} - -GoRoute _mobileEditPropertyPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileEditPropertyScreen.routeName, - pageBuilder: (context, state) { - final args = state.extra as Map; - return MaterialExtendedPage( - fullscreenDialog: true, - child: MobileEditPropertyScreen( - viewId: args[MobileEditPropertyScreen.argViewId], - field: args[MobileEditPropertyScreen.argField], - ), - ); - }, - ); -} - -GoRoute _mobileCalendarEventsPageRoute() { - return GoRoute( - path: MobileCalendarEventsScreen.routeName, - parentNavigatorKey: AppGlobals.rootNavKey, - pageBuilder: (context, state) { - final args = state.extra as Map; - - return MaterialExtendedPage( - child: MobileCalendarEventsScreen( - calendarBloc: args[MobileCalendarEventsScreen.calendarBlocKey], - date: args[MobileCalendarEventsScreen.calendarDateKey], - events: args[MobileCalendarEventsScreen.calendarEventsKey], - rowCache: args[MobileCalendarEventsScreen.calendarRowCacheKey], - viewId: args[MobileCalendarEventsScreen.calendarViewIdKey], - ), - ); - }, - ); -} - -GoRoute _desktopHomeScreenRoute() { - return GoRoute( - path: DesktopHomeScreen.routeName, - pageBuilder: (context, state) { - return CustomTransitionPage( - child: const DesktopHomeScreen(), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ); -} - -GoRoute _workspaceErrorScreenRoute() { - return GoRoute( - path: WorkspaceErrorScreen.routeName, - pageBuilder: (context, state) { - final args = state.extra as Map; - return CustomTransitionPage( - child: WorkspaceErrorScreen( - error: args[WorkspaceErrorScreen.argError], - userFolder: args[WorkspaceErrorScreen.argUserFolder], - ), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ); -} - -GoRoute _skipLogInScreenRoute() { - return GoRoute( - path: SkipLogInScreen.routeName, - pageBuilder: (context, state) { - return CustomTransitionPage( - child: const SkipLogInScreen(), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ); -} - -GoRoute _signInScreenRoute() { - return GoRoute( - path: SignInScreen.routeName, - pageBuilder: (context, state) { - return CustomTransitionPage( - child: const SignInScreen(), - transitionsBuilder: _buildFadeTransition, - transitionDuration: _slowDuration, - ); - }, - ); -} - -GoRoute _mobileEditorScreenRoute() { - return GoRoute( - path: MobileDocumentScreen.routeName, - parentNavigatorKey: AppGlobals.rootNavKey, - 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, - ), - ); - }, - ); -} - -GoRoute _mobileChatScreenRoute() { - return GoRoute( - path: MobileChatScreen.routeName, - parentNavigatorKey: AppGlobals.rootNavKey, - pageBuilder: (context, state) { - final id = state.uri.queryParameters[MobileChatScreen.viewId]!; - final title = state.uri.queryParameters[MobileChatScreen.viewTitle]; - - return MaterialExtendedPage( - child: MobileChatScreen(id: id, title: title), - ); - }, - ); -} - -GoRoute _mobileGridScreenRoute() { - return GoRoute( - path: MobileGridScreen.routeName, - parentNavigatorKey: AppGlobals.rootNavKey, - pageBuilder: (context, state) { - final id = state.uri.queryParameters[MobileGridScreen.viewId]!; - final title = state.uri.queryParameters[MobileGridScreen.viewTitle]; - final arguments = state.uri.queryParameters[MobileGridScreen.viewArgs]; - - return MaterialExtendedPage( - child: MobileGridScreen( - id: id, - title: title, - arguments: arguments != null ? jsonDecode(arguments) : null, - ), - ); - }, - ); -} - -GoRoute _mobileBoardScreenRoute() { - return GoRoute( - path: MobileBoardScreen.routeName, - parentNavigatorKey: AppGlobals.rootNavKey, - pageBuilder: (context, state) { - final id = state.uri.queryParameters[MobileBoardScreen.viewId]!; - final title = state.uri.queryParameters[MobileBoardScreen.viewTitle]; - return MaterialExtendedPage( - child: MobileBoardScreen( - id: id, - title: title, - ), - ); - }, - ); -} - -GoRoute _mobileCalendarScreenRoute() { - return GoRoute( - path: MobileCalendarScreen.routeName, - parentNavigatorKey: AppGlobals.rootNavKey, - pageBuilder: (context, state) { - final id = state.uri.queryParameters[MobileCalendarScreen.viewId]!; - final title = state.uri.queryParameters[MobileCalendarScreen.viewTitle]!; - return MaterialExtendedPage( - child: MobileCalendarScreen( - id: id, - title: title, - ), - ); - }, - ); -} - -GoRoute _mobileCardDetailScreenRoute() { - return GoRoute( - 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 databaseController = - extra[MobileRowDetailPage.argDatabaseController]; - final rowId = extra[MobileRowDetailPage.argRowId]!; - - if (kDebugMode) { - _dynamicValues = extra; - } - - return MaterialExtendedPage( - child: MobileRowDetailPage( - databaseController: databaseController, - rowId: rowId, - ), - ); - }, - ); -} - -GoRoute _mobileDateCellEditScreenRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileDateCellEditScreen.routeName, - pageBuilder: (context, state) { - final args = state.extra as Map; - final controller = args[MobileDateCellEditScreen.dateCellController]; - final fullScreen = args[MobileDateCellEditScreen.fullScreen]; - return CustomTransitionPage( - transitionsBuilder: (_, __, ___, child) => child, - fullscreenDialog: true, - opaque: false, - barrierDismissible: true, - barrierColor: Theme.of(context).bottomSheetTheme.modalBarrierColor, - child: MobileDateCellEditScreen( - controller: controller, - showAsFullScreen: fullScreen ?? true, - ), - ); - }, - ); -} - -GoRoute _rootRoute(Widget child) { - return GoRoute( - path: '/', - redirect: (context, state) async { - // Every time before navigating to splash screen, we check if user is already logged in desktop. It is used to skip showing splash screen when user just changes appearance settings like theme mode. - final userResponse = await getIt().getUser(); - final routeName = userResponse.fold( - (user) => DesktopHomeScreen.routeName, - (error) => null, - ); - if (routeName != null && !UniversalPlatform.isMobile) return routeName; - - return null; - }, - // Root route is SplashScreen. - // It needs LaunchConfiguration as a parameter, so we get it from ApplicationWidget's child. - pageBuilder: (context, state) => MaterialExtendedPage( - child: child, - ), - ); -} - -Widget _buildFadeTransition( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, -) => - FadeTransition(opacity: animation, child: child); - -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..f252d48621 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/hot_key.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/hot_key.dart @@ -1,5 +1,4 @@ import 'package:hotkey_manager/hotkey_manager.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../startup.dart'; @@ -8,13 +7,6 @@ class HotKeyTask extends LaunchTask { @override Future initialize(LaunchContext context) async { - // the hotkey manager is not supported on mobile - if (UniversalPlatform.isMobile) { - return; - } await hotKeyManager.unregisterAll(); } - - @override - Future dispose() async {} } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart b/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart index 9a75607d74..97e1e6dce9 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart @@ -1,8 +1,6 @@ -import 'package:appflowy/plugins/ai_chat/chat.dart'; -import 'package:appflowy/plugins/database/calendar/calendar.dart'; -import 'package:appflowy/plugins/database/board/board.dart'; -import 'package:appflowy/plugins/database/grid/grid.dart'; -import 'package:appflowy/plugins/database_document/database_document_plugin.dart'; +import 'package:appflowy/plugins/database_view/calendar/calendar.dart'; +import 'package:appflowy/plugins/database_view/board/board.dart'; +import 'package:appflowy/plugins/database_view/grid/grid.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/plugins/blank/blank.dart'; @@ -26,20 +24,5 @@ class PluginLoadTask extends LaunchTask { builder: CalendarPluginBuilder(), config: CalendarPluginConfig(), ); - registerPlugin( - builder: DatabaseDocumentPluginBuilder(), - config: DatabaseDocumentPluginConfig(), - ); - registerPlugin( - builder: DatabaseDocumentPluginBuilder(), - config: DatabaseDocumentPluginConfig(), - ); - registerPlugin( - builder: AIChatPluginBuilder(), - config: AIChatPluginConfig(), - ); } - - @override - Future dispose() async {} } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/localization.dart b/frontend/appflowy_flutter/lib/startup/tasks/localization.dart index 83950ffc1b..8a28a2ded1 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/localization.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/localization.dart @@ -8,9 +8,5 @@ class InitLocalizationTask extends LaunchTask { @override Future initialize(LaunchContext context) async { await EasyLocalization.ensureInitialized(); - EasyLocalization.logger.enableBuildModes = []; } - - @override - Future dispose() async {} } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart b/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart deleted file mode 100644 index f35c955b3d..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:leak_tracker/leak_tracker.dart'; - -import '../startup.dart'; - -bool enableMemoryLeakDetect = false; -bool dumpMemoryLeakPerSecond = false; - -void dumpMemoryLeak({ - LeakType type = LeakType.notDisposed, -}) async { - final details = await LeakTracking.collectLeaks(); - details.dumpDetails(type); -} - -class MemoryLeakDetectorTask extends LaunchTask { - MemoryLeakDetectorTask(); - - Timer? _timer; - - @override - Future initialize(LaunchContext context) async { - if (!kDebugMode || !enableMemoryLeakDetect) { - return; - } - - LeakTracking.start(); - LeakTracking.phase = const PhaseSettings( - leakDiagnosticConfig: LeakDiagnosticConfig( - collectRetainingPathForNotGCed: true, - collectStackTraceOnStart: true, - ), - ); - - FlutterMemoryAllocations.instance.addListener((p0) { - LeakTracking.dispatchObjectEvent(p0.toMap()); - }); - - // dump memory leak per second if needed - if (dumpMemoryLeakPerSecond) { - _timer = Timer.periodic(const Duration(seconds: 1), (_) async { - final summary = await LeakTracking.checkLeaks(); - if (summary.isEmpty) { - return; - } - - dumpMemoryLeak(); - }); - } - } - - @override - Future dispose() async { - if (!kDebugMode || !enableMemoryLeakDetect) { - return; - } - - if (dumpMemoryLeakPerSecond) { - _timer?.cancel(); - _timer = null; - } - - LeakTracking.stop(); - } -} - -extension on LeakType { - String get desc => switch (this) { - LeakType.notDisposed => 'not disposed', - LeakType.notGCed => 'not GCed', - LeakType.gcedLate => 'GCed late' - }; -} - -final _dumpablePackages = [ - 'package:appflowy/', - 'package:appflowy_editor/', -]; - -extension on Leaks { - void dumpDetails(LeakType type) { - final summary = '${type.desc}: ${switch (type) { - LeakType.notDisposed => '${notDisposed.length}', - LeakType.notGCed => '${notGCed.length}', - LeakType.gcedLate => '${gcedLate.length}' - }}'; - debugPrint(summary); - final details = switch (type) { - LeakType.notDisposed => notDisposed, - LeakType.notGCed => notGCed, - LeakType.gcedLate => gcedLate - }; - - // only dump the code in appflowy - for (final value in details) { - final stack = value.context![ContextKeys.startCallstack]! as StackTrace; - final stackInAppFlowy = stack - .toString() - .split('\n') - .where( - (stack) => - // ignore current file call stack - !stack.contains('memory_leak_detector') && - _dumpablePackages.any((pkg) => stack.contains(pkg)), - ) - .join('\n'); - - // ignore the untreatable leak - if (stackInAppFlowy.isEmpty) { - continue; - } - - final object = value.type; - debugPrint(''' -$object ${type.desc} -$stackInAppFlowy -'''); - } - } -} 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..16dc8ba6da 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,25 +17,5 @@ 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 - Future dispose() async {} } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/platform_service.dart b/frontend/appflowy_flutter/lib/startup/tasks/platform_service.dart index 4206d447db..7b76eebf50 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/platform_service.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/platform_service.dart @@ -9,11 +9,6 @@ class InitPlatformServiceTask extends LaunchTask { @override Future initialize(LaunchContext context) async { - return getIt().start(); - } - - @override - Future dispose() async { - await getIt().stop(); + getIt().start(); } } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart index 9e8f9df49a..d7d7993af7 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart @@ -1,17 +1,9 @@ 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'; -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 'platform_service.dart'; +export 'load_plugin.dart'; +export 'hot_key.dart'; +export 'platform_error_catcher.dart'; export 'windows.dart'; +export 'localization.dart'; +export 'supabase_task.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/recent_service_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/recent_service_task.dart deleted file mode 100644 index 5d632903cc..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/recent_service_task.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/recent/prelude.dart'; -import 'package:appflowy_backend/log.dart'; - -class RecentServiceTask extends LaunchTask { - const RecentServiceTask(); - - @override - Future initialize(LaunchContext context) async => - Log.info('[CachedRecentService] Initialized'); - - @override - Future dispose() async => getIt().dispose(); -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index c406dd161a..d1e99f0d94 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -1,83 +1,68 @@ -import 'dart:convert'; import 'dart:io'; -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/env/env.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; -import 'package:path/path.dart' as path; +import 'package:appflowy_backend/env_serde.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; import '../startup.dart'; class InitRustSDKTask extends LaunchTask { const InitRustSDKTask({ - this.customApplicationPath, + this.directory, }); // Customize the RustSDK initialization path - final Directory? customApplicationPath; + final Directory? directory; @override LaunchTaskType get type => LaunchTaskType.dataProcessing; @override Future initialize(LaunchContext context) async { - final root = await getApplicationSupportDirectory(); - final applicationPath = await appFlowyApplicationDataDirectory(); - final dir = customApplicationPath ?? applicationPath; - final deviceId = await getDeviceId(); + final dir = directory ?? await appFlowyApplicationDataDirectory(); - // Pass the environment variables to the Rust SDK - final env = _makeAppFlowyConfiguration( - root.path, - context.config.version, - dir.path, - applicationPath.path, - deviceId, - rustEnvs: context.config.rustEnvs, - ); - await context.getIt().init(jsonEncode(env.toJson())); + context.getIt().setEnv(getAppFlowyEnv()); + await context.getIt().init(dir); } - - @override - Future dispose() async {} } -AppFlowyConfiguration _makeAppFlowyConfiguration( - String root, - String appVersion, - String customAppPath, - String originAppPath, - String deviceId, { - required Map rustEnvs, -}) { - final env = getIt(); - return AppFlowyConfiguration( - root: root, - app_version: appVersion, - custom_app_path: customAppPath, - origin_app_path: originAppPath, - device_id: deviceId, - platform: Platform.operatingSystem, - authenticator_type: env.authenticatorType.value, - appflowy_cloud_config: env.appflowyCloudConfig, - envs: rustEnvs, +AppFlowyEnv getAppFlowyEnv() { + final supabaseConfig = SupabaseConfiguration( + url: Env.supabaseUrl, + key: Env.supabaseKey, + jwt_secret: Env.supabaseJwtSecret, + ); + + final collabTableConfig = + CollabTableConfig(enable: true, table_name: Env.supabaseCollabTable); + + final supbaseDBConfig = SupabaseDBConfig( + url: Env.supabaseUrl, + key: Env.supabaseKey, + jwt_secret: Env.supabaseJwtSecret, + collab_table_config: collabTableConfig, + ); + + return AppFlowyEnv( + supabase_config: supabaseConfig, + supabase_db_config: supbaseDBConfig, ); } /// The default directory to store the user data. The directory can be /// customized by the user via the [ApplicationDataStorage] Future appFlowyApplicationDataDirectory() async { - switch (integrationMode()) { + switch (integrationEnv()) { case IntegrationMode.develop: final Directory documentsDir = await getApplicationSupportDirectory() - .then((directory) => directory.create()); - return Directory(path.join(documentsDir.path, 'data_dev')); + ..create(); + return Directory(path.join(documentsDir.path, 'data_dev')).create(); case IntegrationMode.release: final Directory documentsDir = await getApplicationSupportDirectory(); - return Directory(path.join(documentsDir.path, 'data')); - case IntegrationMode.unitTest: + return Directory(path.join(documentsDir.path, 'data')).create(); + case IntegrationMode.test: 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..f915fd883e --- /dev/null +++ b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart @@ -0,0 +1,49 @@ +import 'package:appflowy/core/config/config.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import '../startup.dart'; + +bool isSupabaseEnable = false; +bool isSupabaseInitialized = false; + +class InitSupabaseTask extends LaunchTask { + const InitSupabaseTask({ + required this.url, + required this.anonKey, + required this.key, + required this.jwtSecret, + this.collabTable = "", + }); + + final String url; + final String anonKey; + final String key; + final String jwtSecret; + final String collabTable; + + @override + Future initialize(LaunchContext context) async { + if (url.isEmpty || anonKey.isEmpty || jwtSecret.isEmpty || key.isEmpty) { + isSupabaseEnable = false; + Log.info('Supabase config is empty, skip init supabase.'); + return; + } + if (isSupabaseInitialized) { + return; + } + await Supabase.initialize( + url: url, + anonKey: anonKey, + debug: false, + ); + await Config.setSupabaseConfig( + url: url, + key: key, + secret: jwtSecret, + anonKey: anonKey, + ); + isSupabaseEnable = true; + isSupabaseInitialized = true; + } +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/windows.dart b/frontend/appflowy_flutter/lib/startup/tasks/windows.dart index 20b8b0b56e..0dec1377fd 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/windows.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/windows.dart @@ -1,135 +1,54 @@ -import 'dart:async'; import 'dart:ui'; 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: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'}); + const InitAppWindowTask({ + this.minimumSize = const Size(800, 600), + this.title = 'AppFlowy', + }); + final Size minimumSize; final String title; - final windowSizeManager = WindowSizeManager(); @override Future initialize(LaunchContext context) async { // Don't initialize on mobile or web. - if (!defaultTargetPlatform.isDesktop || context.env.isIntegrationTest) { + if (!defaultTargetPlatform.isDesktop) { return; } await windowManager.ensureInitialized(); windowManager.addListener(this); - final windowSize = await windowSizeManager.getSize(); + Size windowSize = await WindowSizeManager().getSize(); + if (context.env.isIntegrationTest()) { + windowSize = const Size(1600, 1200); + } + final windowOptions = WindowOptions( size: windowSize, minimumSize: const Size( WindowSizeManager.minWindowWidth, WindowSizeManager.minWindowHeight, ), - maximumSize: const Size( - WindowSizeManager.maxWindowWidth, - WindowSizeManager.maxWindowHeight, - ), title: title, ); - final position = await windowSizeManager.getPosition(); - - if (UniversalPlatform.isWindows) { - doWhenWindowReady(() async { - appWindow.minSize = windowOptions.minimumSize; - appWindow.maxSize = windowOptions.maximumSize; - appWindow.size = windowSize; - - if (position != null) { - appWindow.position = position; - } - - 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 (position != null) { - await windowManager.setPosition(position); - } - }); - } - - unawaited( - windowSizeManager.getScaleFactor().then( - (v) => ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => v, - ), - ); - } - - @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); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); } @override Future onWindowResize() async { - super.onWindowResize(); - final currentWindowSize = await windowManager.getSize(); - return windowSizeManager.setSize(currentWindowSize); - } - - @override - void onWindowMoved() async { - super.onWindowMoved(); - - final position = await windowManager.getPosition(); - return windowSizeManager.setPosition(position); - } - - @override - Future dispose() async { - windowManager.removeListener(this); + WindowSizeManager().saveSize(currentWindowSize); } } diff --git a/frontend/appflowy_flutter/lib/user/application/anon_user_bloc.dart b/frontend/appflowy_flutter/lib/user/application/anon_user_bloc.dart deleted file mode 100644 index 292760ca4b..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/anon_user_bloc.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/code.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'; - -part 'anon_user_bloc.freezed.dart'; - -class AnonUserBloc extends Bloc { - AnonUserBloc() : super(AnonUserState.initial()) { - on((event, emit) async { - await event.when( - initial: () async { - await _loadHistoricalUsers(); - }, - didLoadAnonUsers: (List anonUsers) { - emit(state.copyWith(anonUsers: anonUsers)); - }, - openAnonUser: (anonUser) async { - await UserBackendService.openAnonUser(); - emit(state.copyWith(openedAnonUser: anonUser)); - }, - ); - }); - } - - Future _loadHistoricalUsers() async { - final result = await UserBackendService.getAnonUser(); - result.fold( - (anonUser) { - add(AnonUserEvent.didLoadAnonUsers([anonUser])); - }, - (error) { - if (error.code != ErrorCode.RecordNotFound) { - Log.error(error); - } - }, - ); - } -} - -@freezed -class AnonUserEvent with _$AnonUserEvent { - const factory AnonUserEvent.initial() = _Initial; - const factory AnonUserEvent.didLoadAnonUsers( - List historicalUsers, - ) = _DidLoadHistoricalUsers; - const factory AnonUserEvent.openAnonUser(UserProfilePB anonUser) = - _OpenHistoricalUser; -} - -@freezed -class AnonUserState with _$AnonUserState { - const factory AnonUserState({ - required List anonUsers, - required UserProfilePB? openedAnonUser, - }) = _AnonUserState; - - factory AnonUserState.initial() => const AnonUserState( - anonUsers: [], - openedAnonUser: null, - ); -} 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 deleted file mode 100644 index 4f4cece9bb..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/core/helpers/url_launcher.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/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/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'auth_error.dart'; - -class AppFlowyCloudAuthService implements AuthService { - AppFlowyCloudAuthService(); - - final BackendAuthService _backendAuthService = BackendAuthService( - AuthTypePB.Server, - ); - - @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 { - return _backendAuthService.signInWithEmailPassword( - email: email, - password: password, - params: params, - ); - } - - @override - Future> signUpWithOAuth({ - required String platform, - Map params = const {}, - }) async { - final provider = ProviderTypePBExtension.fromPlatform(platform); - - // Get the oauth url from the backend - final result = await UserEventGetOauthURLWithProvider( - OauthProviderPB.create()..provider = provider, - ).send(); - - return result.fold( - (data) async { - // Open the webview with oauth url - final uri = Uri.parse(data.oauthUrl); - final isSuccess = await afLaunchUri( - uri, - mode: LaunchMode.externalApplication, - webOnlyWindowName: '_self', - ); - - final completer = Completer>(); - if (isSuccess) { - // The [AppFlowyCloudDeepLink] must be registered before using the - // [AppFlowyCloudAuthService]. - if (getIt.isRegistered()) { - getIt().registerCompleter(completer); - } else { - throw Exception('AppFlowyCloudDeepLink is not registered'); - } - } else { - completer.complete( - FlowyResult.failure(AuthError.unableToGetDeepLink), - ); - } - - return completer.future; - }, - (r) => FlowyResult.failure(r), - ); - } - - @override - Future signOut() async { - await _backendAuthService.signOut(); - } - - @override - Future> signUpAsGuest({ - Map params = const {}, - }) async { - return _backendAuthService.signUpAsGuest(); - } - - @override - Future> signInWithMagicLink({ - required String email, - Map params = const {}, - }) async { - return _backendAuthService.signInWithMagicLink( - email: email, - params: params, - ); - } - - @override - Future> signInWithPasscode({ - required String email, - required String passcode, - }) async { - return _backendAuthService.signInWithPasscode( - email: email, - passcode: passcode, - ); - } - - @override - Future> getUser() async { - return UserBackendService.getCurrentUserProfile(); - } -} - -extension ProviderTypePBExtension on ProviderTypePB { - static ProviderTypePB fromPlatform(String platform) { - switch (platform) { - case 'github': - return ProviderTypePB.Github; - case 'google': - 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 deleted file mode 100644 index 8be71dc648..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart +++ /dev/null @@ -1,118 +0,0 @@ -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/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:flowy_infra/uuid.dart'; -import 'package:flutter/material.dart'; - -/// Only used for testing. -class AppFlowyCloudMockAuthService implements AuthService { - AppFlowyCloudMockAuthService({String? email}) - : userEmail = email ?? "${uuid()}@appflowy.io"; - - final String userEmail; - - final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthTypePB.Server); - - @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 { - final payload = SignInUrlPayloadPB.create() - ..authenticator = AuthTypePB.Server - // don't use nanoid here, the gotrue server will transform the email - ..email = userEmail; - - final deviceId = await getDeviceId(); - final getSignInURLResult = await UserEventGenerateSignInURL(payload).send(); - - return getSignInURLResult.fold( - (urlPB) async { - final payload = OauthSignInPB( - authenticator: AuthTypePB.Server, - map: { - AuthServiceMapKeys.signInURL: urlPB.signInUrl, - AuthServiceMapKeys.deviceId: deviceId, - }, - ); - 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); - }, - ); - return value; - }); - }, - (r) { - debugPrint("mock auth service error: $r"); - return FlowyResult.failure(r); - }, - ); - } - - @override - Future signOut() async { - await _appFlowyAuthService.signOut(); - } - - @override - Future> signUpAsGuest({ - Map params = const {}, - }) async { - return _appFlowyAuthService.signUpAsGuest(); - } - - @override - Future> signInWithMagicLink({ - required String email, - Map params = const {}, - }) async { - throw UnimplementedError(); - } - - @override - 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/appflowy_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart new file mode 100644 index 0000000000..302d31a75c --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart @@ -0,0 +1,87 @@ +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.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' + show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; + +import '../../../generated/locale_keys.g.dart'; + +class AppFlowyAuthService implements AuthService { + @override + Future> signIn({ + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) async { + final request = SignInPayloadPB.create() + ..email = email + ..password = password + ..authType = authType; + final response = UserEventSignIn(request).send(); + return response.then((value) => value.swap()); + } + + @override + Future> signUp({ + required String name, + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) async { + final request = SignUpPayloadPB.create() + ..name = name + ..email = email + ..password = password + ..authType = authType; + final response = await UserEventSignUp(request).send().then( + (value) => value.swap(), + ); + return response; + } + + @override + Future signOut({ + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) async { + final payload = SignOutPB()..authType = authType; + await UserEventSignOut(payload).send(); + return; + } + + @override + Future> signUpAsGuest({ + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) { + const password = "AppFlowy123@"; + final uid = uuid(); + final userEmail = "$uid@appflowy.io"; + return signUp( + name: LocaleKeys.defaultUsername.tr(), + password: password, + email: userEmail, + ); + } + + @override + Future> signUpWithOAuth({ + required String platform, + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) { + throw UnimplementedError(); + } + + @override + Future> getUser() async { + return UserBackendService.getCurrentUserProfile(); + } +} 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..f9629e1547 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart @@ -1,20 +1,19 @@ -import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; class AuthError { - static final signInWithOauthError = FlowyError() - ..msg = 'sign in with oauth error -10003' - ..code = ErrorCode.UserUnauthorized; + static final supabaseSignInError = FlowyError() + ..msg = 'supabase sign in error' + ..code = -10001; - static final emptyDeepLink = FlowyError() - ..msg = 'Unexpected empty DeepLink' - ..code = ErrorCode.UnexpectedCalendarFieldType; + static final supabaseSignUpError = FlowyError() + ..msg = 'supabase sign up error' + ..code = -10002; - static final deepLinkError = FlowyError() - ..msg = 'DeepLink error' - ..code = ErrorCode.Internal; + static final supabaseSignInWithOauthError = FlowyError() + ..msg = 'supabase sign in with oauth error' + ..code = -10003; - static final unableToGetDeepLink = FlowyError() - ..msg = 'Unable to get the deep link' - ..code = ErrorCode.Internal; + static final supabaseGetUserError = FlowyError() + ..msg = 'supabase sign in with oauth error' + ..code = -10003; } 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..70c7fd73f9 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart @@ -1,97 +1,51 @@ 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:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; +import 'package:dartz/dartz.dart'; class AuthServiceMapKeys { const AuthServiceMapKeys._(); - static const String email = 'email'; - static const String deviceId = 'device_id'; - static const String signInURL = 'sign_in_url'; + // for supabase auth use only. + static const String uuid = 'uuid'; } -/// `AuthService` is an abstract class that defines methods related to user authentication. -/// -/// This service provides various methods for user sign-in, sign-up, -/// OAuth-based registration, and other related functionalities. abstract class AuthService { - /// Authenticates a user with their email and password. - /// - /// - `email`: The email address of the user. - /// - `password`: The password of the user. - /// - `params`: Additional parameters for authentication (optional). - /// /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - - Future> - signInWithEmailPassword({ + Future> signIn({ required String email, required String password, - Map params, + AuthTypePB authType, + Map map, }); - /// Registers a new user with their name, email, and password. - /// - /// - `name`: The name of the user. - /// - `email`: The email address of the user. - /// - `password`: The password of the user. - /// - `params`: Additional parameters for registration (optional). - /// /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> signUp({ + Future> signUp({ required String name, required String email, required String password, - Map params, + AuthTypePB authType, + Map map, }); - /// Registers a new user with an OAuth platform. /// - /// - `platform`: The OAuth platform name. - /// - `params`: Additional parameters for OAuth registration (optional). - /// - /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> signUpWithOAuth({ + Future> signUpWithOAuth({ required String platform, - Map params, + AuthTypePB authType, + Map map, }); - /// Registers a user as a guest. - /// - /// - `params`: Additional parameters for guest registration (optional). - /// - /// Returns a default [UserProfilePB]. - Future> signUpAsGuest({ - Map params, + /// Returns a default [UserProfilePB] + Future> signUpAsGuest({ + AuthTypePB authType, + Map map, }); - /// Authenticates a user with a magic link sent to their email. /// - /// - `email`: The email address of the user. - /// - `params`: Additional parameters for authentication with magic link (optional). - /// - /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> signInWithMagicLink({ - required String email, - Map params, + Future signOut({ + AuthTypePB authType, }); - /// 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(); - - /// Retrieves the currently authenticated user's profile. - /// - /// Returns [UserProfilePB] if the user has signed in, otherwise returns [FlowyError]. - Future> getUser(); + /// Returns [UserProfilePB] if the user has sign in, otherwise returns null. + Future> getUser(); } 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 deleted file mode 100644 index cab8cd170c..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.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/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 '../../../generated/locale_keys.g.dart'; -import 'device_id.dart'; - -class BackendAuthService implements AuthService { - BackendAuthService(this.authType); - - final AuthTypePB authType; - - @override - Future> - signInWithEmailPassword({ - required String email, - required String password, - Map params = const {}, - }) async { - final request = SignInPayloadPB.create() - ..email = email - ..password = password - ..authType = authType - ..deviceId = await getDeviceId(); - return UserEventSignInWithEmailPassword(request).send(); - } - - @override - Future> signUp({ - required String name, - required String email, - required String password, - Map params = const {}, - }) async { - final request = SignUpPayloadPB.create() - ..name = name - ..email = email - ..password = password - ..authType = authType - ..deviceId = await getDeviceId(); - final response = await UserEventSignUp(request).send().then( - (value) => value, - ); - return response; - } - - @override - Future signOut({ - Map params = const {}, - }) async { - await UserEventSignOut().send(); - return; - } - - @override - Future> signUpAsGuest({ - Map params = const {}, - }) async { - const password = "Guest!@123456"; - final userEmail = "anon@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 - ..deviceId = await getDeviceId(); - final response = await UserEventSignUp(request).send().then( - (value) => value, - ); - return response; - } - - @override - Future> signUpWithOAuth({ - required String platform, - AuthTypePB authType = AuthTypePB.Local, - Map params = const {}, - }) async { - return FlowyResult.failure( - FlowyError.create() - ..code = ErrorCode.Internal - ..msg = "Unsupported sign up action", - ); - } - - @override - Future> getUser() async { - return UserBackendService.getCurrentUserProfile(); - } - - @override - Future> signInWithMagicLink({ - required String email, - Map params = const {}, - }) async { - // 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/device_id.dart b/frontend/appflowy_flutter/lib/user/application/auth/device_id.dart deleted file mode 100644 index 2d7fe580ae..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/auth/device_id.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/services.dart'; - -final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - -Future getDeviceId() async { - if (integrationMode().isTest) { - return "test_device_id"; - } - - String? deviceId; - try { - if (Platform.isAndroid) { - final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - deviceId = androidInfo.device; - } else if (Platform.isIOS) { - final IosDeviceInfo iosInfo = await deviceInfo.iosInfo; - deviceId = iosInfo.identifierForVendor; - } else if (Platform.isMacOS) { - final MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo; - deviceId = macInfo.systemGUID; - } else if (Platform.isWindows) { - final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo; - deviceId = windowsInfo.deviceId; - } else if (Platform.isLinux) { - final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo; - deviceId = linuxInfo.machineId; - } - } on PlatformException { - Log.error('Failed to get platform version'); - } - return deviceId ?? ''; -} 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..6a28a44981 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart @@ -0,0 +1,221 @@ +import 'dart:async'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/prelude.dart'; +import 'package:appflowy/user/application/auth/appflowy_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_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:dartz/dartz.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 AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService(); + + @override + Future> signUp({ + required String name, + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signUp( + name: name, + email: email, + password: password, + ); + } + + // fetch the uuid from supabase. + final response = await _auth.signUp( + email: email, + password: password, + ); + final uuid = response.user?.id; + if (uuid == null) { + return left(AuthError.supabaseSignUpError); + } + // assign the uuid to our backend service. + // and will transfer this logic to backend later. + return _appFlowyAuthService.signUp( + name: name, + email: email, + password: password, + authType: authType, + map: { + AuthServiceMapKeys.uuid: uuid, + }, + ); + } + + @override + Future> signIn({ + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signIn( + email: email, + password: password, + ); + } + + try { + final response = await _auth.signInWithPassword( + email: email, + password: password, + ); + final uuid = response.user?.id; + if (uuid == null) { + return Left(AuthError.supabaseSignInError); + } + return _appFlowyAuthService.signIn( + email: email, + password: password, + authType: authType, + map: { + AuthServiceMapKeys.uuid: uuid, + }, + ); + } on AuthException catch (e) { + Log.error(e); + return Left(AuthError.supabaseSignInError); + } + } + + @override + Future> signUpWithOAuth({ + required String platform, + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signUpWithOAuth( + platform: platform, + ); + } + final provider = platform.toProvider(); + final completer = Completer>(); + late final StreamSubscription subscription; + subscription = _auth.onAuthStateChange.listen((event) async { + if (event.event != AuthChangeEvent.signedIn) { + completer.complete(left(AuthError.supabaseSignInWithOauthError)); + } else { + final user = await getSupabaseUser(); + final Either response = await user.fold( + (l) => left(l), + (r) async => await setupAuth(map: {AuthServiceMapKeys.uuid: r.id}), + ); + completer.complete(response); + } + subscription.cancel(); + }); + final Map query = {}; + if (provider == Provider.google) { + query['access_type'] = 'offline'; + query['prompt'] = 'consent'; + } + final response = await _auth.signInWithOAuth( + provider, + queryParams: query, + redirectTo: + 'io.appflowy.appflowy-flutter://login-callback', // can't use underscore here. + ); + if (!response) { + completer.complete(left(AuthError.supabaseSignInWithOauthError)); + } + return completer.future; + } + + @override + Future signOut({ + AuthTypePB authType = AuthTypePB.Supabase, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signOut(); + } + await _auth.signOut(); + await _appFlowyAuthService.signOut( + authType: authType, + ); + } + + @override + Future> signUpAsGuest({ + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + // supabase don't support guest login. + // so, just forward to our backend. + return _appFlowyAuthService.signUpAsGuest(); + } + + @override + Future> getUser() async { + final loginType = await getIt() + .get(KVKeys.loginType) + .then((value) => value.toOption().toNullable()); + if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) { + return _appFlowyAuthService.getUser(); + } + final user = await getSupabaseUser(); + return user.map((r) => r.toUserProfile()); + } + + Future> getSupabaseUser() async { + final user = _auth.currentUser; + if (user == null) { + return left(AuthError.supabaseGetUserError); + } + return Right(user); + } + + Future> setupAuth({ + required Map map, + }) async { + final payload = ThirdPartyAuthPB( + authType: AuthTypePB.Supabase, + map: map, + ); + return UserEventThirdPartyAuth(payload) + .send() + .then((value) => value.swap()); + } +} + +extension on User { + UserProfilePB toUserProfile() { + return UserProfilePB() + ..email = email ?? '' + ..token = this.id; + } +} + +extension on String { + Provider toProvider() { + switch (this) { + case 'github': + return Provider.github; + case 'google': + return Provider.google; + case 'discord': + return Provider.discord; + default: + throw UnimplementedError(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart b/frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart deleted file mode 100644 index 53ba478b41..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'notification_filter_bloc.freezed.dart'; - -class NotificationFilterBloc - extends Bloc { - NotificationFilterBloc() : super(const NotificationFilterState()) { - on((event, emit) async { - event.when( - reset: () => emit(const NotificationFilterState()), - toggleShowUnreadsOnly: () => emit( - state.copyWith(showUnreadsOnly: !state.showUnreadsOnly), - ), - ); - }); - } -} - -@freezed -class NotificationFilterEvent with _$NotificationFilterEvent { - const factory NotificationFilterEvent.toggleShowUnreadsOnly() = - _ToggleShowUnreadsOnly; - - const factory NotificationFilterEvent.reset() = _Reset; -} - -@freezed -class NotificationFilterState with _$NotificationFilterState { - const NotificationFilterState._(); - - const factory NotificationFilterState({ - @Default(false) bool showUnreadsOnly, - }) = _NotificationFilterState; - - // If state is not default values, then there are custom changes - bool get hasFilters => showUnreadsOnly != 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 723ded57e2..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart +++ /dev/null @@ -1,183 +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', - ); - - return result.fold( - (data) => FlowyResult.success(data['has_password'] ?? false), - (error) => FlowyResult.failure(error), - ); - } - - /// 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/prelude.dart b/frontend/appflowy_flutter/lib/user/application/prelude.dart index 2bd1c18cb7..ddacb654d3 100644 --- a/frontend/appflowy_flutter/lib/user/application/prelude.dart +++ b/frontend/appflowy_flutter/lib/user/application/prelude.dart @@ -1,4 +1,4 @@ -export 'auth/backend_auth_service.dart'; +export 'auth/appflowy_auth_service.dart'; export './sign_in_bloc.dart'; export './sign_up_bloc.dart'; export './splash_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart deleted file mode 100644 index 24f53e48e8..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart +++ /dev/null @@ -1,537 +0,0 @@ -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'; -import 'package:appflowy/user/application/user_settings_service.dart'; -import 'package:appflowy/util/int64_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/notification/notification_service.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:bloc/bloc.dart'; -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(); - - _dispatch(); - } - - late final ActionNavigationBloc _actionBloc; - late final ReminderService _reminderService; - late final Timer timer; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - started: () async { - Log.info('Start fetching reminders'); - - 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'), - ); - }, - remove: (reminderId) async { - final result = await _reminderService.removeReminder( - reminderId: reminderId, - ); - - result.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', - ), - ); - }, - 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 result = await _reminderService.addReminder( - reminder: reminder, - ); - - return result.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'), - ); - }, - addById: (reminderId, objectId, scheduledAt, meta) async => add( - ReminderEvent.add( - reminder: ReminderPB( - id: reminderId, - objectId: objectId, - title: LocaleKeys.reminderNotification_title.tr(), - message: LocaleKeys.reminderNotification_message.tr(), - scheduledAt: scheduledAt, - isAck: scheduledAt.toDateTime().isBefore(DateTime.now()), - meta: meta, - ), - ), - ), - update: (updateObject) async { - final reminder = state.reminders.firstWhereOrNull( - (r) => r.id == updateObject.id, - ); - - if (reminder == null) { - return; - } - - final newReminder = updateObject.merge(a: reminder); - final failureOrUnit = await _reminderService.updateReminder( - reminder: newReminder, - ); - - 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', - ), - ); - }, - pressReminder: (reminderId, path, view) { - final reminder = - state.reminders.firstWhereOrNull((r) => r.id == reminderId); - - if (reminder == null) { - return; - } - - add( - ReminderEvent.update( - ReminderUpdate( - id: reminderId, - isRead: state.pastReminders.contains(reminder), - ), - ), - ); - - String? rowId; - if (view?.layout != ViewLayoutPB.Document) { - rowId = reminder.meta[ReminderMetaKeys.rowId]; - } - - final action = NavigationAction( - objectId: reminder.objectId, - arguments: { - ActionArgumentKeys.view: view, - ActionArgumentKeys.nodePath: path, - ActionArgumentKeys.rowId: rowId, - }, - ); - - if (!isClosed) { - _actionBloc.add( - ActionNavigationEvent.performAction( - action: action, - nextActions: [ - action.copyWith( - type: rowId != null - ? ActionType.openRow - : ActionType.jumpToBlock, - ), - ], - ), - ); - } - }, - 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), - (_) async { - final now = DateTime.now(); - - for (final reminder in state.upcomingReminders) { - if (reminder.isAck) { - continue; - } - - final scheduledAt = reminder.scheduledAt.toDateTime(); - - if (scheduledAt.isBefore(now)) { - final notificationSettings = - await UserSettingsBackendService().getNotificationSettings(); - if (notificationSettings.notificationsEnabled) { - NotificationMessage( - identifier: reminder.id, - title: LocaleKeys.reminderNotification_title.tr(), - body: LocaleKeys.reminderNotification_message.tr(), - onClick: () => _actionBloc.add( - ActionNavigationEvent.performAction( - action: NavigationAction(objectId: reminder.objectId), - ), - ), - ); - } - - add( - ReminderEvent.update( - ReminderUpdate(id: reminder.id, isAck: true), - ), - ); - } - } - }, - ); - } -} - -@freezed -class ReminderEvent with _$ReminderEvent { - // On startup we fetch all reminders and upcoming ones - const factory ReminderEvent.started() = _Started; - - // Remove a reminder - const factory ReminderEvent.remove({required String reminderId}) = _Remove; - - // Add a reminder - const factory ReminderEvent.add({required ReminderPB reminder}) = _Add; - - // Add a reminder - const factory ReminderEvent.addById({ - required String reminderId, - required String objectId, - required Int64 scheduledAt, - @Default(null) Map? meta, - }) = _AddById; - - // 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 - 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 -/// a [ReminderPB] -/// -class ReminderUpdate { - ReminderUpdate({ - required this.id, - this.isAck, - this.isRead, - this.scheduledAt, - this.includeTime, - this.isArchived, - this.date, - }); - - final String id; - final bool? isAck; - 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}; - 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, - scheduledAt: scheduledAt != null - ? Int64(scheduledAt!.millisecondsSinceEpoch ~/ 1000) - : a.scheduledAt, - isAck: isAcknowledged, - isRead: isRead ?? a.isRead, - title: a.title, - message: a.message, - meta: meta, - ); - } -} - -class ReminderState { - ReminderState({List? reminders}) { - _reminders = reminders ?? []; - - pastReminders = []; - upcomingReminders = []; - - if (_reminders.isEmpty) { - hasUnreads = false; - return; - } - - final now = DateTime.now(); - - bool hasUnreadReminders = false; - for (final reminder in _reminders) { - final scheduledDate = DateTime.fromMillisecondsSinceEpoch( - reminder.scheduledAt.toInt() * 1000, - ); - - if (scheduledDate.isBefore(now)) { - pastReminders.add(reminder); - - if (!hasUnreadReminders && !reminder.isRead) { - hasUnreadReminders = true; - } - } else { - upcomingReminders.add(reminder); - } - } - - hasUnreads = hasUnreadReminders; - } - - late final List _reminders; - List get reminders => _reminders.unique((e) => e.id); - - late final List pastReminders; - late final List upcomingReminders; - late final bool hasUnreads; - - ReminderState copyWith({List? reminders}) => - ReminderState(reminders: reminders ?? _reminders); -} diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart deleted file mode 100644 index 1b5aeaeb43..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; - -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 { - bool? get includeTime { - final String? includeTimeStr = meta[ReminderMetaKeys.includeTime]; - - 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/reminder/reminder_service.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_service.dart deleted file mode 100644 index eed44f9ced..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_service.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -/// Interface for a Reminder Service that handles -/// communication to the backend -/// -abstract class IReminderService { - Future, FlowyError>> fetchReminders(); - - Future> removeReminder({ - required String reminderId, - }); - - Future> addReminder({ - required ReminderPB reminder, - }); - - Future> updateReminder({ - required ReminderPB reminder, - }); -} - -class ReminderService implements IReminderService { - const ReminderService(); - - @override - Future> addReminder({ - required ReminderPB reminder, - }) async { - final unitOrFailure = await UserEventCreateReminder(reminder).send(); - - return unitOrFailure; - } - - @override - Future> updateReminder({ - required ReminderPB reminder, - }) async { - final unitOrFailure = await UserEventUpdateReminder(reminder).send(); - - return unitOrFailure; - } - - @override - Future, FlowyError>> fetchReminders() async { - final resultOrFailure = await UserEventGetAllReminders().send(); - - return resultOrFailure.fold( - (s) => FlowyResult.success(s.items), - (e) => FlowyResult.failure(e), - ); - } - - @override - Future> removeReminder({ - required String reminderId, - }) async { - final request = ReminderIdentifierPB(id: reminderId); - final unitOrFailure = await UserEventRemoveReminder(request).send(); - - return unitOrFailure; - } -} 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..a95530a5c0 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -1,323 +1,147 @@ -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:dartz/dartz.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; part 'sign_in_bloc.freezed.dart'; class SignInBloc extends Bloc { - SignInBloc(this.authService) : super(SignInState.initial()) { - if (isAppFlowyCloudEnabled) { - deepLinkStateListener = - getIt().subscribeDeepLinkLoadingState((value) { - if (isClosed) return; - - add(SignInEvent.deepLinkStateChange(value)); - }); - } - - 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, - ), - deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result), - cancel: () { - emit( - state.copyWith( - isSubmitting: false, - emailError: null, - passwordError: null, - successOrFail: null, - ), - ); - }, - emailChanged: (email) async { - emit( - state.copyWith( - email: email, - emailError: null, - successOrFail: null, - ), - ); - }, - passwordChanged: (password) async { - emit( - state.copyWith( - password: password, - passwordError: null, - successOrFail: null, - ), - ); - }, - switchLoginType: (type) { - emit(state.copyWith(loginType: type)); - }, - ); - }, - ); - } - final AuthService authService; - VoidCallback? deepLinkStateListener; - - @override - Future close() { - deepLinkStateListener?.call(); - if (isAppFlowyCloudEnabled && deepLinkStateListener != null) { - getIt().unsubscribeDeepLinkLoadingState( - deepLinkStateListener!, + SignInBloc(this.authService) : super(SignInState.initial()) { + on((event, emit) async { + await event.map( + signedInWithUserEmailAndPassword: (e) async { + await _performActionOnSignIn( + state, + emit, + ); + }, + signedInWithOAuth: (value) async => + await _performActionOnSignInWithOAuth( + state, + emit, + value.platform, + ), + signedInAsGuest: (value) async => await _performActionOnSignInAsGuest( + state, + emit, + ), + emailChanged: (EmailChanged value) async { + emit( + state.copyWith( + email: value.email, + emailError: none(), + successOrFail: none(), + ), + ); + }, + passwordChanged: (PasswordChanged value) async { + emit( + state.copyWith( + password: value.password, + passwordError: none(), + successOrFail: none(), + ), + ); + }, ); - } - return super.close(); + }); } - Future _onDeepLinkStateChange( + Future _performActionOnSignIn( + SignInState state, Emitter emit, - DeepLinkResult result, ) async { - final deepLinkState = result.state; - - switch (deepLinkState) { - case DeepLinkState.none: - break; - case DeepLinkState.loading: - emit( - state.copyWith( - isSubmitting: true, - emailError: null, - passwordError: null, - successOrFail: null, - ), - ); - case DeepLinkState.finish: - final newState = result.result?.fold( - (s) => state.copyWith( - isSubmitting: false, - successOrFail: FlowyResult.success(s), - ), - (f) => _stateFromCode(f), - ); - if (newState != null) { - emit(newState); - } - } - } - - Future _onSignInWithEmailAndPassword( - Emitter emit, { - required String email, - required String password, - }) async { - final result = await authService.signInWithEmailPassword( - email: email, - password: password, + final result = await authService.signIn( + email: state.email ?? '', + password: state.password ?? '', ); emit( result.fold( - (gotrueTokenResponse) { - getIt().passGotrueTokenResponse( - gotrueTokenResponse, - ); - return state.copyWith( - isSubmitting: false, - ); - }, - (error) => _stateFromCode(error), - ), - ); - } - - Future _onSignInWithOAuth( - Emitter emit, { - required String platform, - }) async { - emit( - state.copyWith( - isSubmitting: true, - emailError: null, - passwordError: null, - successOrFail: null, - ), - ); - - final result = await authService.signUpWithOAuth(platform: platform); - emit( - result.fold( + (error) => stateFromCode(error), (userProfile) => state.copyWith( isSubmitting: false, - successOrFail: FlowyResult.success(userProfile), + successOrFail: some(left(userProfile)), ), - (error) => _stateFromCode(error), ), ); } - 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'); - + Future _performActionOnSignInWithOAuth( + SignInState state, + Emitter emit, + String platform, + ) async { emit( state.copyWith( isSubmitting: true, - emailError: null, - passwordError: null, - successOrFail: null, + emailError: none(), + passwordError: none(), + successOrFail: none(), ), ); - final result = await authService.signInWithMagicLink(email: email); - + final result = await authService.signUpWithOAuth( + platform: platform, + ); emit( result.fold( + (error) => stateFromCode(error), (userProfile) => state.copyWith( isSubmitting: false, + successOrFail: some(left(userProfile)), ), - (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, - ); - }, - (error) => _stateFromCode(error), - ), - ); - } - - Future _onSignInAsGuest( + Future _performActionOnSignInAsGuest( + SignInState state, Emitter emit, ) async { emit( state.copyWith( isSubmitting: true, - emailError: null, - passwordError: null, - successOrFail: null, + emailError: none(), + passwordError: none(), + successOrFail: none(), ), ); final result = await authService.signUpAsGuest(); emit( result.fold( + (error) => stateFromCode(error), (userProfile) => state.copyWith( isSubmitting: false, - successOrFail: FlowyResult.success(userProfile), + successOrFail: some(left(userProfile)), ), - (error) => _stateFromCode(error), ), ); } - SignInState _stateFromCode(FlowyError error) { - Log.error('SignInState _stateFromCode: ${error.msg}'); - - switch (error.code) { + SignInState stateFromCode(FlowyError error) { + switch (ErrorCode.valueOf(error.code)!) { case ErrorCode.EmailFormatInvalid: return state.copyWith( isSubmitting: false, - emailError: error.msg, - passwordError: null, + emailError: some(error.msg), + passwordError: none(), ); case ErrorCode.PasswordFormatInvalid: return state.copyWith( isSubmitting: false, - passwordError: error.msg, - 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), - ), + passwordError: some(error.msg), + emailError: none(), ); default: return state.copyWith( isSubmitting: false, - successOrFail: FlowyResult.failure( - FlowyError(msg: LocaleKeys.signIn_generalError.tr()), - ), + successOrFail: some(right(error)), ); } } @@ -325,42 +149,13 @@ 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.deepLinkStateChange(DeepLinkResult result) = - DeepLinkStateChange; - - 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 -// this type is only for the UI to know which form to show -enum LoginType { - signIn, - signUp, + const factory SignInEvent.signedInWithUserEmailAndPassword() = + SignedInWithUserEmailAndPassword; + const factory SignInEvent.signedInWithOAuth(String platform) = + SignedInWithOAuth; + const factory SignInEvent.signedInAsGuest() = SignedInAsGuest; + const factory SignInEvent.emailChanged(String email) = EmailChanged; + const factory SignInEvent.passwordChanged(String password) = PasswordChanged; } @freezed @@ -369,16 +164,15 @@ class SignInState with _$SignInState { String? email, String? password, required bool isSubmitting, - required String? passwordError, - required String? emailError, - required FlowyResult? successOrFail, - @Default(LoginType.signIn) LoginType loginType, + required Option passwordError, + required Option emailError, + required Option> successOrFail, }) = _SignInState; - factory SignInState.initial() => const SignInState( + factory SignInState.initial() => SignInState( isSubmitting: false, - passwordError: null, - emailError: null, - successOrFail: null, + passwordError: none(), + emailError: none(), + successOrFail: none(), ); } diff --git a/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart index 1935d00c6d..ca884cdecc 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart @@ -1,67 +1,60 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:dartz/dartz.dart'; +import 'package:easy_localization/easy_localization.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_bloc/flutter_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; part 'sign_up_bloc.freezed.dart'; class SignUpBloc extends Bloc { - SignUpBloc(this.authService) : super(SignUpState.initial()) { - _dispatch(); - } - final AuthService authService; - - void _dispatch() { - on( - (event, emit) async { - await event.map( - signUpWithUserEmailAndPassword: (e) async { - await _performActionOnSignUp(emit); - }, - emailChanged: (_EmailChanged value) async { - emit( - state.copyWith( - email: value.email, - emailError: null, - successOrFail: null, - ), - ); - }, - passwordChanged: (_PasswordChanged value) async { - emit( - state.copyWith( - password: value.password, - passwordError: null, - successOrFail: null, - ), - ); - }, - repeatPasswordChanged: (_RepeatPasswordChanged value) async { - emit( - state.copyWith( - repeatedPassword: value.password, - repeatPasswordError: null, - successOrFail: null, - ), - ); - }, - ); - }, - ); + SignUpBloc(this.authService) : super(SignUpState.initial()) { + on((event, emit) async { + await event.map( + signUpWithUserEmailAndPassword: (e) async { + await _performActionOnSignUp(emit); + }, + emailChanged: (_EmailChanged value) async { + emit( + state.copyWith( + email: value.email, + emailError: none(), + successOrFail: none(), + ), + ); + }, + passwordChanged: (_PasswordChanged value) async { + emit( + state.copyWith( + password: value.password, + passwordError: none(), + successOrFail: none(), + ), + ); + }, + repeatPasswordChanged: (_RepeatPasswordChanged value) async { + emit( + state.copyWith( + repeatedPassword: value.password, + repeatPasswordError: none(), + successOrFail: none(), + ), + ); + }, + ); + }); } Future _performActionOnSignUp(Emitter emit) async { emit( state.copyWith( isSubmitting: true, - successOrFail: null, + successOrFail: none(), ), ); @@ -71,7 +64,7 @@ class SignUpBloc extends Bloc { emit( state.copyWith( isSubmitting: false, - passwordError: LocaleKeys.signUp_emptyPasswordError.tr(), + passwordError: some(LocaleKeys.signUp_emptyPasswordError.tr()), ), ); return; @@ -81,7 +74,8 @@ class SignUpBloc extends Bloc { emit( state.copyWith( isSubmitting: false, - repeatPasswordError: LocaleKeys.signUp_repeatPasswordEmptyError.tr(), + repeatPasswordError: + some(LocaleKeys.signUp_repeatPasswordEmptyError.tr()), ), ); return; @@ -91,7 +85,8 @@ class SignUpBloc extends Bloc { emit( state.copyWith( isSubmitting: false, - repeatPasswordError: LocaleKeys.signUp_unmatchedPasswordError.tr(), + repeatPasswordError: + some(LocaleKeys.signUp_unmatchedPasswordError.tr()), ), ); return; @@ -99,8 +94,8 @@ class SignUpBloc extends Bloc { emit( state.copyWith( - passwordError: null, - repeatPasswordError: null, + passwordError: none(), + repeatPasswordError: none(), ), ); @@ -111,38 +106,38 @@ class SignUpBloc extends Bloc { ); emit( result.fold( + (error) => stateFromCode(error), (profile) => state.copyWith( isSubmitting: false, - successOrFail: FlowyResult.success(profile), - emailError: null, - passwordError: null, - repeatPasswordError: null, + successOrFail: some(left(profile)), + emailError: none(), + passwordError: none(), + repeatPasswordError: none(), ), - (error) => stateFromCode(error), ), ); } SignUpState stateFromCode(FlowyError error) { - switch (error.code) { + switch (ErrorCode.valueOf(error.code)!) { case ErrorCode.EmailFormatInvalid: return state.copyWith( isSubmitting: false, - emailError: error.msg, - passwordError: null, - successOrFail: null, + emailError: some(error.msg), + passwordError: none(), + successOrFail: none(), ); case ErrorCode.PasswordFormatInvalid: return state.copyWith( isSubmitting: false, - passwordError: error.msg, - emailError: null, - successOrFail: null, + passwordError: some(error.msg), + emailError: none(), + successOrFail: none(), ); default: return state.copyWith( isSubmitting: false, - successOrFail: FlowyResult.failure(error), + successOrFail: some(right(error)), ); } } @@ -165,17 +160,17 @@ class SignUpState with _$SignUpState { String? password, String? repeatedPassword, required bool isSubmitting, - required String? passwordError, - required String? repeatPasswordError, - required String? emailError, - required FlowyResult? successOrFail, + required Option passwordError, + required Option repeatPasswordError, + required Option emailError, + required Option> successOrFail, }) = _SignUpState; - factory SignUpState.initial() => const SignUpState( + factory SignUpState.initial() => SignUpState( isSubmitting: false, - passwordError: null, - repeatPasswordError: null, - emailError: null, - successOrFail: null, + passwordError: none(), + repeatPasswordError: none(), + emailError: none(), + successOrFail: none(), ); } diff --git a/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart b/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart index 584c730fba..f603e1cf53 100644 --- a/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart @@ -13,8 +13,8 @@ class SplashBloc extends Bloc { getUser: (val) async { final response = await getIt().getUser(); final authState = response.fold( - (user) => AuthState.authenticated(user), (error) => AuthState.unauthenticated(error), + (user) => AuthState.authenticated(user), ); emit(state.copyWith(auth: authState)); }, diff --git a/frontend/appflowy_flutter/lib/user/application/user_auth_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_auth_listener.dart deleted file mode 100644 index 612c1eacd5..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/user_auth_listener.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/user_notification.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-user/auth.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart' - as user; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -class UserAuthStateListener { - void Function(String)? _onInvalidAuth; - void Function()? _didSignIn; - StreamSubscription? _subscription; - UserNotificationParser? _userParser; - - void start({ - void Function(String)? onInvalidAuth, - void Function()? didSignIn, - }) { - _onInvalidAuth = onInvalidAuth; - _didSignIn = didSignIn; - - _userParser = UserNotificationParser( - id: "auth_state_change_notification", - callback: _userNotificationCallback, - ); - _subscription = RustStreamReceiver.listen((observable) { - _userParser?.parse(observable); - }); - } - - Future stop() async { - _userParser = null; - await _subscription?.cancel(); - _onInvalidAuth = null; - } - - void _userNotificationCallback( - user.UserNotification ty, - FlowyResult result, - ) { - switch (ty) { - case user.UserNotification.UserAuthStateChanged: - result.fold( - (payload) { - final pb = AuthStateChangedPB.fromBuffer(payload); - switch (pb.state) { - case AuthStatePB.AuthStateSignIn: - _didSignIn?.call(); - break; - case AuthStatePB.InvalidAuth: - _onInvalidAuth?.call(pb.message); - break; - default: - break; - } - }, - (r) => Log.error(r), - ); - break; - default: - break; - } - } -} diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index d3ebe0201b..a06e398bc5 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -1,65 +1,43 @@ 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'; import 'package:appflowy/core/notification/user_notification.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'dart:typed_data'; +import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart' as user; import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flowy_infra/notifier.dart'; -typedef DidUpdateUserWorkspaceCallback = void Function( - UserWorkspacePB workspace, -); -typedef DidUpdateUserWorkspacesCallback = void Function( - RepeatedUserWorkspacePB workspaces, -); -typedef UserProfileNotifyValue = FlowyResult; -typedef DidUpdateUserWorkspaceSetting = void Function( - WorkspaceSettingsPB settings, -); +typedef UserProfileNotifyValue = Either; +typedef AuthNotifyValue = Either; class UserListener { + StreamSubscription? _subscription; + PublishNotifier? _authNotifier = PublishNotifier(); + PublishNotifier? _profileNotifier = PublishNotifier(); + + UserNotificationParser? _userParser; + final UserProfilePB _userProfile; UserListener({ required UserProfilePB userProfile, }) : _userProfile = userProfile; - final UserProfilePB _userProfile; - - UserNotificationParser? _userParser; - StreamSubscription? _subscription; - PublishNotifier? _profileNotifier = PublishNotifier(); - - /// Update notification about _all_ of the users workspaces - /// - DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated; - - /// Update notification about _one_ workspace - /// - DidUpdateUserWorkspaceCallback? onUserWorkspaceUpdated; - DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated; - void start({ + void Function(AuthNotifyValue)? onAuthChanged, void Function(UserProfileNotifyValue)? onProfileUpdated, - DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated, - void Function(UserWorkspacePB)? onUserWorkspaceUpdated, - DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated, }) { if (onProfileUpdated != null) { _profileNotifier?.addPublishListener(onProfileUpdated); } - this.onUserWorkspaceListUpdated = onUserWorkspaceListUpdated; - this.onUserWorkspaceUpdated = onUserWorkspaceUpdated; - this.onUserWorkspaceSettingUpdated = onUserWorkspaceSettingUpdated; + if (onAuthChanged != null) { + _authNotifier?.addPublishListener(onAuthChanged); + } _userParser = UserNotificationParser( id: _userProfile.id.toString(), @@ -75,36 +53,21 @@ class UserListener { await _subscription?.cancel(); _profileNotifier?.dispose(); _profileNotifier = null; + + _authNotifier?.dispose(); + _authNotifier = null; } void _userNotificationCallback( user.UserNotification ty, - FlowyResult result, + Either result, ) { switch (ty) { case user.UserNotification.DidUpdateUserProfile: result.fold( - (payload) => _profileNotifier?.value = - FlowyResult.success(UserProfilePB.fromBuffer(payload)), - (error) => _profileNotifier?.value = FlowyResult.failure(error), - ); - break; - case user.UserNotification.DidUpdateUserWorkspaces: - result.map( - (r) { - final value = RepeatedUserWorkspacePB.fromBuffer(r); - onUserWorkspaceListUpdated?.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)), + (payload) => + _profileNotifier?.value = left(UserProfilePB.fromBuffer(payload)), + (error) => _profileNotifier?.value = right(error), ); break; default: @@ -113,21 +76,37 @@ class UserListener { } } -typedef WorkspaceLatestNotifyValue = FlowyResult; +typedef WorkspaceListNotifyValue = Either, FlowyError>; +typedef WorkspaceSettingNotifyValue = Either; -class FolderListener { - FolderListener(); - - final PublishNotifier _latestChangedNotifier = +class UserWorkspaceListener { + PublishNotifier? _authNotifier = PublishNotifier(); + PublishNotifier? _workspacesChangedNotifier = + PublishNotifier(); + PublishNotifier? _settingChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; + UserWorkspaceListener({ + required UserProfilePB userProfile, + }); + void start({ - void Function(WorkspaceLatestNotifyValue)? onLatestUpdated, + void Function(AuthNotifyValue)? onAuthChanged, + void Function(WorkspaceListNotifyValue)? onWorkspacesUpdated, + void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, }) { - if (onLatestUpdated != null) { - _latestChangedNotifier.addPublishListener(onLatestUpdated); + if (onAuthChanged != null) { + _authNotifier?.addPublishListener(onAuthChanged); + } + + if (onWorkspacesUpdated != null) { + _workspacesChangedNotifier?.addPublishListener(onWorkspacesUpdated); + } + + if (onSettingUpdated != null) { + _settingChangedNotifier?.addPublishListener(onSettingUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -140,14 +119,15 @@ class FolderListener { void _handleObservableType( FolderNotification ty, - FlowyResult result, + Either result, ) { switch (ty) { + case FolderNotification.DidCreateWorkspace: case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _latestChangedNotifier.value = - FlowyResult.success(WorkspaceLatestPB.fromBuffer(payload)), - (error) => _latestChangedNotifier.value = FlowyResult.failure(error), + (payload) => _settingChangedNotifier?.value = + left(WorkspaceSettingPB.fromBuffer(payload)), + (error) => _settingChangedNotifier?.value = right(error), ); break; default: @@ -157,6 +137,13 @@ class FolderListener { Future stop() async { await _listener?.stop(); - _latestChangedNotifier.dispose(); + _workspacesChangedNotifier?.dispose(); + _workspacesChangedNotifier = null; + + _settingChangedNotifier?.dispose(); + _settingChangedNotifier = null; + + _authNotifier?.dispose(); + _authNotifier = null; } } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 3ec181e009..3fb1312b85 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -1,45 +1,31 @@ import 'dart:async'; -import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:dartz/dartz.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:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/foundation.dart'; -abstract class IUserBackendService { - Future> cancelSubscription( - String workspaceId, - SubscriptionPlanPB plan, - String? reason, - ); - 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}); +class UserBackendService { + UserBackendService({ + required this.userId, + }); final Int64 userId; - static Future> + static Future> getCurrentUserProfile() async { final result = await UserEventGetUserProfile().send(); - return result; + return result.swap(); } - Future> updateUserProfile({ + Future> updateUserProfile({ String? name, String? password, String? email, String? iconUrl, + String? openAIKey, }) { final payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -59,244 +45,61 @@ class UserBackendService implements IUserBackendService { payload.iconUrl = iconUrl; } + if (openAIKey != null) { + payload.openaiKey = openAIKey; + } + return UserEventUpdateUserProfile(payload).send(); } - Future> deleteWorkspace({ + Future> deleteWorkspace({ required String workspaceId, }) { throw UnimplementedError(); } - static Future> signInWithMagicLink( - String email, - String redirectTo, - ) async { - final payload = MagicLinkSignInPB(email: email, redirectTo: redirectTo); - return UserEventMagicLinkSignIn(payload).send(); + Future> signOut(AuthTypePB authType) { + final payload = SignOutPB()..authType = authType; + return UserEventSignOut(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(); - } - - Future> initUser() async { + Future> initUser() async { return UserEventInitUser().send(); } - static Future> getAnonUser() async { - return UserEventGetAnonUser().send(); - } + Future, FlowyError>> getWorkspaces() { + final request = WorkspaceIdPB.create(); - static Future> openAnonUser() async { - return UserEventOpenAnonUser().send(); - } - - Future, FlowyError>> getWorkspaces() { - return UserEventGetAllWorkspace().send().then((value) { - return value.fold( - (workspaces) => FlowyResult.success(workspaces.items), - (error) => FlowyResult.failure(error), - ); - }); - } - - Future> openWorkspace( - String workspaceId, - AuthTypePB authType, - ) { - final payload = OpenUserWorkspacePB() - ..workspaceId = workspaceId - ..authType = authType; - return UserEventOpenWorkspace(payload).send(); - } - - static Future> getCurrentWorkspace() { - return FolderEventReadCurrentWorkspace().send().then((result) { + return FolderEventReadAllWorkspaces(request).send().then((result) { return result.fold( - (workspace) => FlowyResult.success(workspace), - (error) => FlowyResult.failure(error), + (workspaces) => left(workspaces.items), + (error) => right(error), ); }); } - Future> createUserWorkspace( + Future> openWorkspace(String workspaceId) { + final request = WorkspaceIdPB.create()..value = workspaceId; + return FolderEventOpenWorkspace(request).send().then((result) { + return result.fold( + (workspace) => left(workspace), + (error) => right(error), + ); + }); + } + + Future> createWorkspace( String name, - AuthTypePB authType, + String desc, ) { - final request = CreateWorkspacePB.create() + final request = CreateWorkspacePayloadPB.create() ..name = name - ..authType = authType; - return UserEventCreateWorkspace(request).send(); - } - - Future> deleteWorkspaceById( - String workspaceId, - ) { - final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId; - return UserEventDeleteWorkspace(request).send(); - } - - Future> renameWorkspace( - String workspaceId, - String name, - ) { - final request = RenameWorkspacePB() - ..workspaceId = workspaceId - ..newName = name; - return UserEventRenameWorkspace(request).send(); - } - - Future> updateWorkspaceIcon( - String workspaceId, - String icon, - ) { - final request = ChangeWorkspaceIconPB() - ..workspaceId = workspaceId - ..newIcon = icon; - return UserEventChangeWorkspaceIcon(request).send(); - } - - Future> - getWorkspaceMembers( - String workspaceId, - ) async { - final data = QueryWorkspacePB()..workspaceId = workspaceId; - return UserEventGetWorkspaceMembers(data).send(); - } - - Future> addWorkspaceMember( - String workspaceId, - String email, - ) async { - final data = AddWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email; - return UserEventAddWorkspaceMember(data).send(); - } - - Future> inviteWorkspaceMember( - String workspaceId, - String email, { - AFRolePB? role, - }) async { - final data = WorkspaceMemberInvitationPB() - ..workspaceId = workspaceId - ..inviteeEmail = email; - if (role != null) { - data.role = role; - } - return UserEventInviteWorkspaceMember(data).send(); - } - - Future> removeWorkspaceMember( - String workspaceId, - String email, - ) async { - final data = RemoveWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email; - return UserEventRemoveWorkspaceMember(data).send(); - } - - Future> updateWorkspaceMember( - String workspaceId, - String email, - AFRolePB role, - ) async { - final data = UpdateWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email - ..role = role; - return UserEventUpdateWorkspaceMember(data).send(); - } - - Future> leaveWorkspace( - String workspaceId, - ) async { - final data = UserWorkspaceIdPB.create()..workspaceId = workspaceId; - return UserEventLeaveWorkspace(data).send(); - } - - static Future> - getWorkspaceSubscriptionInfo(String workspaceId) { - final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId; - return UserEventGetWorkspaceSubscriptionInfo(params).send(); - } - - Future> - getWorkspaceMember() async { - final data = WorkspaceMemberIdPB.create()..uid = userId; - - return UserEventGetMemberInfo(data).send(); - } - - @override - Future> createSubscription( - String workspaceId, - SubscriptionPlanPB plan, - ) { - final request = SubscribeWorkspacePB() - ..workspaceId = workspaceId - ..recurringInterval = RecurringIntervalPB.Year - ..workspaceSubscriptionPlan = plan - ..successUrl = - '${kDebugMode ? _baseBetaUrl : _baseProdUrl}/after-payment?plan=${plan.toRecognizable()}'; - 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; - } - - 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(); + ..desc = desc; + return FolderEventCreateWorkspace(request).send().then((result) { + return result.fold( + (workspace) => left(workspace), + (error) => right(error), + ); + }); } } diff --git a/frontend/appflowy_flutter/lib/user/application/user_settings_service.dart b/frontend/appflowy_flutter/lib/user/application/user_settings_service.dart index f9718a9134..ae1debfc5b 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_settings_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_settings_service.dart @@ -1,60 +1,31 @@ -import 'package:appflowy_backend/appflowy_backend.dart'; +import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/appflowy_backend.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/user_setting.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; class UserSettingsBackendService { Future getAppearanceSetting() async { final result = await UserEventGetAppearanceSetting().send(); return result.fold( - (AppearanceSettingsPB setting) => setting, - (error) => - throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty), + (AppearanceSettingsPB setting) { + return setting; + }, + (error) { + throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty); + }, ); } - Future> getUserSetting() { + Future> getUserSetting() { return UserEventGetUserSetting().send(); } - Future> setAppearanceSetting( + Future> setAppearanceSetting( AppearanceSettingsPB setting, ) { return UserEventSetAppearanceSetting(setting).send(); } - - Future getDateTimeSettings() async { - final result = await UserEventGetDateTimeSettings().send(); - - return result.fold( - (DateTimeSettingsPB setting) => setting, - (error) => - throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty), - ); - } - - Future> setDateTimeSettings( - DateTimeSettingsPB settings, - ) async { - return UserEventSetDateTimeSettings(settings).send(); - } - - Future> setNotificationSettings( - NotificationSettingsPB settings, - ) async { - return UserEventSetNotificationSettings(settings).send(); - } - - Future getNotificationSettings() async { - final result = await UserEventGetNotificationSettings().send(); - - return result.fold( - (NotificationSettingsPB setting) => setting, - (error) => - throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty), - ); - } } diff --git a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart deleted file mode 100644 index 7ff50dbd02..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:appflowy/plugins/database/application/defines.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_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'workspace_error_bloc.freezed.dart'; - -class WorkspaceErrorBloc - extends Bloc { - WorkspaceErrorBloc({required this.userFolder, required FlowyError error}) - : super(WorkspaceErrorState.initial(error)) { - _dispatch(); - } - - final UserFolderPB userFolder; - - void _dispatch() { - on( - (event, emit) async { - event.when( - init: () { - // _loadSnapshots(); - }, - didResetWorkspace: (result) { - result.fold( - (_) { - emit( - state.copyWith( - loadingState: LoadingState.finish(result), - workspaceState: const WorkspaceState.reset(), - ), - ); - }, - (err) { - emit(state.copyWith(loadingState: LoadingState.finish(result))); - }, - ); - }, - logout: () { - emit( - state.copyWith( - workspaceState: const WorkspaceState.logout(), - ), - ); - }, - ); - }, - ); - } -} - -@freezed -class WorkspaceErrorEvent with _$WorkspaceErrorEvent { - const factory WorkspaceErrorEvent.init() = _Init; - const factory WorkspaceErrorEvent.logout() = _DidLogout; - const factory WorkspaceErrorEvent.didResetWorkspace( - FlowyResult result, - ) = _DidResetWorkspace; -} - -@freezed -class WorkspaceErrorState with _$WorkspaceErrorState { - const factory WorkspaceErrorState({ - required FlowyError initialError, - LoadingState? loadingState, - required WorkspaceState workspaceState, - }) = _WorkspaceErrorState; - - factory WorkspaceErrorState.initial(FlowyError error) => WorkspaceErrorState( - initialError: error, - workspaceState: const WorkspaceState.initial(), - ); -} - -@freezed -class WorkspaceState with _$WorkspaceState { - const factory WorkspaceState.initial() = _Initial; - const factory WorkspaceState.logout() = _Logout; - const factory WorkspaceState.reset() = _Reset; - const factory WorkspaceState.createNewWorkspace() = _NewWorkspace; - const factory WorkspaceState.restoreFromSnapshot() = _RestoreFromSnapshot; -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart deleted file mode 100644 index ddb1a07f96..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.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/user/application/anon_user_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 AnonUserList extends StatelessWidget { - const AnonUserList({required this.didOpenUser, super.key}); - - final VoidCallback didOpenUser; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => AnonUserBloc() - ..add( - const AnonUserEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - if (state.anonUsers.isEmpty) { - return const SizedBox.shrink(); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Opacity( - opacity: 0.6, - child: FlowyText.regular( - LocaleKeys.settings_menu_historicalUserListTooltip.tr(), - fontSize: 13, - maxLines: null, - ), - ), - const VSpace(6), - Expanded( - child: ListView.builder( - itemBuilder: (context, index) { - final user = state.anonUsers[index]; - return AnonUserItem( - key: ValueKey(user.id), - user: user, - isSelected: false, - didOpenUser: didOpenUser, - ); - }, - itemCount: state.anonUsers.length, - ), - ), - ], - ); - } - }, - ), - ); - } -} - -class AnonUserItem extends StatelessWidget { - const AnonUserItem({ - super.key, - required this.user, - required this.isSelected, - required this.didOpenUser, - }); - - final UserProfilePB user; - final bool isSelected; - final VoidCallback didOpenUser; - - @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 child = SizedBox( - height: 30, - child: FlowyButton( - disable: isDisabled, - text: FlowyText.medium( - desc, - fontSize: 12, - ), - rightIcon: icon, - onTap: () { - context.read().add(AnonUserEvent.openAnonUser(user)); - didOpenUser(); - }, - ), - ); - return child; - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart b/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart new file mode 100644 index 0000000000..f495a79a3a --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart @@ -0,0 +1,291 @@ +import 'dart:io'; + +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.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:fluttertoast/fluttertoast.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../../generated/locale_keys.g.dart'; +import '../../../startup/startup.dart'; +import '../../../workspace/application/settings/settings_location_cubit.dart'; +import '../../../workspace/presentation/home/toast.dart'; + +enum _FolderPage { + options, + create, + open, +} + +class FolderWidget extends StatefulWidget { + const FolderWidget({ + super.key, + required this.createFolderCallback, + }); + + final Future Function() createFolderCallback; + + @override + State createState() => _FolderWidgetState(); +} + +class _FolderWidgetState extends State { + var page = _FolderPage.options; + + @override + Widget build(BuildContext context) { + return _mapIndexToWidget(context); + } + + Widget _mapIndexToWidget(BuildContext context) { + switch (page) { + case _FolderPage.options: + return FolderOptionsWidget( + onPressedOpen: () { + _openFolder(); + }, + ); + case _FolderPage.create: + return CreateFolderWidget( + onPressedBack: () { + setState(() => page = _FolderPage.options); + }, + onPressedCreate: widget.createFolderCallback, + ); + case _FolderPage.open: + return Container(); + } + } + + Future _openFolder() async { + final path = await getIt().getDirectoryPath(); + if (path != null) { + await getIt().setCustomPath(path); + await widget.createFolderCallback(); + setState(() {}); + } + } +} + +class FolderOptionsWidget extends StatelessWidget { + const FolderOptionsWidget({ + super.key, + required this.onPressedOpen, + }); + + final VoidCallback onPressedOpen; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: getIt().getPath(), + builder: (context, result) { + final subtitle = result.hasData ? result.data! : ''; + return _FolderCard( + icon: const FlowySvg(name: 'common/archive'), + title: LocaleKeys.settings_files_defineWhereYourDataIsStored.tr(), + subtitle: subtitle, + trailing: _buildTextButton( + context, + LocaleKeys.settings_files_set.tr(), + onPressedOpen, + ), + ); + }, + ); + } +} + +class CreateFolderWidget extends StatefulWidget { + const CreateFolderWidget({ + Key? key, + required this.onPressedBack, + required this.onPressedCreate, + }) : super(key: key); + + final VoidCallback onPressedBack; + final Future Function() onPressedCreate; + + @override + State createState() => CreateFolderWidgetState(); +} + +@visibleForTesting +class CreateFolderWidgetState extends State { + var _folderName = 'appflowy'; + @visibleForTesting + var directory = ''; + + final _fToast = FToast(); + + @override + void initState() { + super.initState(); + _fToast.init(context); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + onPressed: widget.onPressedBack, + icon: const Icon(Icons.arrow_back_rounded), + label: const Text('Back'), + ), + ), + _FolderCard( + title: LocaleKeys.settings_files_location.tr(), + subtitle: LocaleKeys.settings_files_locationDesc.tr(), + trailing: SizedBox( + width: 120, + child: FlowyTextField( + hintText: LocaleKeys.settings_files_folderHintText.tr(), + onChanged: (name) => _folderName = name, + onSubmitted: (name) => setState( + () => _folderName = name, + ), + ), + ), + ), + _FolderCard( + title: LocaleKeys.settings_files_folderPath.tr(), + subtitle: _path, + trailing: _buildTextButton( + context, + LocaleKeys.settings_files_browser.tr(), + () async { + final dir = await getIt().getDirectoryPath(); + if (dir != null) { + setState(() => directory = dir); + } + }, + ), + ), + const VSpace(4.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Row( + children: [ + _buildTextButton( + context, + LocaleKeys.settings_files_create.tr(), + () async { + if (_path.isEmpty) { + _showToast( + LocaleKeys.settings_files_locationCannotBeEmpty.tr(), + ); + } else { + await getIt().setCustomPath(_path); + await widget.onPressedCreate(); + } + }, + ), + ], + ), + ) + ], + ); + } + + String get _path { + if (directory.isEmpty) return ''; + final String path; + if (Platform.isMacOS) { + path = directory.replaceAll('/Volumes/Macintosh HD', ''); + } else { + path = directory; + } + return '$path/$_folderName'; + } + + void _showToast(String message) { + _fToast.showToast( + child: FlowyMessageToast(message: message), + gravity: ToastGravity.CENTER, + ); + } +} + +Widget _buildTextButton( + BuildContext context, + String title, + VoidCallback onPressed, +) { + return SizedBox( + width: 60, + child: SecondaryTextButton( + title, + mode: SecondaryTextButtonMode.small, + onPressed: onPressed, + ), + ); +} + +class _FolderCard extends StatelessWidget { + const _FolderCard({ + required this.title, + required this.subtitle, + this.trailing, + this.icon, + }); + + final String title; + final String subtitle; + final Widget? icon; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 16.0, + ), + child: Row( + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 20), + child: icon!, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.regular( + title, + fontSize: FontSizes.s14, + fontFamily: GoogleFonts.poppins( + fontWeight: FontWeight.w500, + ).fontFamily, + ), + const VSpace(4), + FlowyText.regular( + subtitle, + overflow: TextOverflow.ellipsis, + fontSize: FontSizes.s12, + fontFamily: GoogleFonts.poppins( + fontWeight: FontWeight.w300, + ).fontFamily, + ), + ], + ), + ), + if (trailing != null) ...[ + const HSpace(40), + trailing!, + ], + ], + ), + ), + ); + } +} 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 deleted file mode 100644 index ccad6c0a26..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ /dev/null @@ -1,36 +0,0 @@ -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:flutter/material.dart'; - -void handleOpenWorkspaceError(BuildContext context, FlowyError error) { - Log.error(error); - switch (error.code) { - case ErrorCode.WorkspaceDataNotSync: - final userFolder = UserFolderPB.fromBuffer(error.payload); - getIt().pushWorkspaceErrorScreen(context, userFolder, error); - break; - case ErrorCode.InvalidEncryptSecret: - case ErrorCode.NetworkError: - showToastNotification( - message: error.msg, - type: ToastificationType.error, - ); - break; - default: - showToastNotification( - message: error.msg, - type: ToastificationType.error, - callbacks: ToastificationCallbacks( - onDismissed: (_) { - getIt().signOut(); - runAppFlowy(); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart deleted file mode 100644 index 11f321232e..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart +++ /dev/null @@ -1 +0,0 @@ -export 'handle_open_workspace_error.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/presentation.dart b/frontend/appflowy_flutter/lib/user/presentation/presentation.dart deleted file mode 100644 index baa6753163..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/presentation.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'screens/screens.dart'; -export 'widgets/widgets.dart'; -export 'anon_user.dart'; -export 'router.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index 339c2f29f7..8546172b36 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -1,123 +1,127 @@ -import 'package:appflowy/mobile/presentation/home/mobile_home_page.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/presentation/screens/screens.dart'; -import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/presentation/sign_in_screen.dart'; +import 'package:appflowy/user/presentation/sign_up_screen.dart'; +import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; +import 'package:appflowy/user/presentation/welcome_screen.dart'; +import 'package:appflowy/workspace/presentation/home/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:flowy_infra/time/duration.dart'; +import 'package:flowy_infra_ui/widget/route/animation.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.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) {} - void pushWorkspaceStartScreen( - BuildContext context, - UserProfilePB userProfile, - ) { - getIt().pushWorkspaceStartScreen(context, userProfile); + void pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) { + getIt().pushWelcomeScreen(context, userProfile); } - /// Navigates to the home screen based on the current workspace and platform. - /// - /// This function takes in a [BuildContext] and a [UserProfilePB] object to - /// determine the user's settings and then navigate to the appropriate home screen - /// (`MobileHomeScreen` for mobile platforms, `DesktopHomeScreen` for others). - /// - /// It first fetches the current workspace settings using [FolderEventGetCurrentWorkspace]. - /// If the workspace settings are successfully fetched, it navigates to the home screen. - /// If there's an error, it defaults to the workspace start screen. - /// - /// @param [context] BuildContext for navigating to the appropriate screen. - /// @param [userProfile] UserProfilePB object containing the details of the current user. - /// - Future goHomeScreen( - BuildContext context, - UserProfilePB userProfile, - ) async { - final result = await FolderEventGetCurrentWorkspaceSetting().send(); - result.fold( - (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) { - context.go( - MobileHomeScreen.routeName, - ); - } else { - context.go( - DesktopHomeScreen.routeName, - ); - } - }, - (error) => pushWorkspaceStartScreen(context, userProfile), + void pushSignUpScreen(BuildContext context) { + Navigator.of(context).push( + PageRoutes.fade( + () => SignUpScreen(router: getIt()), + ), ); } - Future pushWorkspaceErrorScreen( + void pushHomeScreenWithWorkSpace( BuildContext context, - UserFolderPB userFolder, - FlowyError error, + UserProfilePB profile, + WorkspaceSettingPB workspaceSetting, + ) { + Navigator.push( + context, + PageRoutes.fade( + () => HomeScreen( + profile, + workspaceSetting, + key: ValueKey(profile.id), + ), + RouteDurations.slow.inMilliseconds * .001, + ), + ); + } + + Future pushHomeScreen( + BuildContext context, + UserProfilePB userProfile, ) async { - await context.push( - WorkspaceErrorScreen.routeName, - extra: { - WorkspaceErrorScreen.argUserFolder: userFolder, - WorkspaceErrorScreen.argError: error, - }, + final result = await FolderEventGetCurrentWorkspace().send(); + result.fold( + (workspaceSettingPB) => pushHomeScreenWithWorkSpace( + context, + userProfile, + workspaceSettingPB, + ), + (r) => pushWelcomeScreen(context, userProfile), ); } } -class SplashRouter { - // Unused for now, it was planed to be used in SignUpScreen. - // To let user choose workspace than navigate to corresponding home screen. - Future pushWorkspaceStartScreen( +class SplashRoute { + Future pushWelcomeScreen( BuildContext context, UserProfilePB userProfile, ) async { - await context.push( - WorkspaceStartScreen.routeName, - extra: { - WorkspaceStartScreen.argUserProfile: userProfile, - }, + final screen = WelcomeScreen(userProfile: userProfile); + await Navigator.of(context).push( + PageRoutes.fade( + () => screen, + RouteDurations.slow.inMilliseconds * .001, + ), ); - final result = await FolderEventGetCurrentWorkspaceSetting().send(); - result.fold( - (workspaceSettingPB) => pushHomeScreen(context), - (r) => null, - ); + FolderEventGetCurrentWorkspace().send().then((result) { + result.fold( + (workspaceSettingPB) => + pushHomeScreen(context, userProfile, workspaceSettingPB), + (r) => null, + ); + }); } void pushHomeScreen( BuildContext context, + UserProfilePB userProfile, + WorkspaceSettingPB workspaceSetting, ) { - if (UniversalPlatform.isMobile) { - context.push( - MobileHomeScreen.routeName, - ); - } else { - context.push( - DesktopHomeScreen.routeName, - ); - } + Navigator.push( + context, + PageRoutes.fade( + () => HomeScreen( + userProfile, + workspaceSetting, + key: ValueKey(userProfile.id), + ), + RouteDurations.slow.inMilliseconds * .001, + ), + ); } - void goHomeScreen( - BuildContext context, - ) { - if (UniversalPlatform.isMobile) { - context.go( - MobileHomeScreen.routeName, - ); - } else { - context.go( - DesktopHomeScreen.routeName, - ); - } + void pushSignInScreen(BuildContext context) { + Navigator.push( + context, + PageRoutes.fade( + () => SignInScreen(router: getIt()), + RouteDurations.slow.inMilliseconds * .001, + ), + ); + } + + void pushSkipLoginScreen(BuildContext context) { + Navigator.push( + context, + PageRoutes.fade( + () => SkipLogInScreen( + router: getIt(), + authService: getIt(), + ), + RouteDurations.slow.inMilliseconds * .001, + ), + ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart deleted file mode 100644 index 2aeba87995..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'sign_in_screen/sign_in_screen.dart'; -export 'skip_log_in_screen.dart'; -export 'splash_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 deleted file mode 100644 index 40901e92e1..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ /dev/null @@ -1,154 +0,0 @@ -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: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, - }); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return BlocBuilder( - builder: (context, state) { - final bottomPadding = UniversalPlatform.isDesktop ? 20.0 : 24.0; - return Scaffold( - appBar: _buildAppBar(), - body: Center( - child: AuthFormContainer( - children: [ - const Spacer(), - - // logo and title - FlowyLogoTitle( - title: LocaleKeys.welcomeText.tr(), - logoSize: Size.square(36), - ), - VSpace(theme.spacing.xxl), - - // continue with email and password - isLocalAuthEnabled - ? const SignInAnonymousButtonV3() - : const ContinueWithEmailAndPassword(), - - VSpace(theme.spacing.xxl), - - // third-party sign in. - if (isAuthEnabled) ...[ - const _OrDivider(), - VSpace(theme.spacing.xxl), - const ThirdPartySignInButtons(), - VSpace(theme.spacing.xxl), - ], - - // sign in agreement - const SignInAgreement(), - - const Spacer(), - - // anonymous sign in and settings - const Row( - mainAxisSize: MainAxisSize.min, - children: [ - DesktopSignInSettingsButton(), - HSpace(20), - SignInAnonymousButtonV2(), - ], - ), - VSpace(bottomPadding), - ], - ), - ), - ); - }, - ); - } - - 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 { - const _OrDivider(); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - children: [ - Flexible( - child: Divider( - thickness: 1, - color: theme.borderColorScheme.greyTertiary, - ), - ), - 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, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart deleted file mode 100644 index 0d74958606..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; -import 'package:appflowy/user/application/sign_in_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 MobileLoadingScreen extends StatelessWidget { - const MobileLoadingScreen({ - super.key, - }); - - @override - Widget build(BuildContext context) { - const double spacing = 16; - - return Scaffold( - appBar: FlowyAppBar( - showDivider: false, - onTapLeading: () => context.read().add( - const SignInEvent.cancel(), - ), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText(LocaleKeys.signIn_signingInText.tr()), - const VSpace(spacing), - const CircularProgressIndicator(), - ], - ), - ), - ); - } -} 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 deleted file mode 100644 index 9eb7d5a965..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ /dev/null @@ -1,119 +0,0 @@ -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/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'; - -class MobileSignInScreen extends StatelessWidget { - const MobileSignInScreen({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final theme = AppFlowyTheme.of(context); - return Scaffold( - resizeToAvoidBottomInset: false, - body: Padding( - padding: const EdgeInsets.symmetric(vertical: 38, 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(), - _buildSettingsButton(context), - ], - ), - ), - ); - }, - ); - } - - Widget _buildThirdPartySignInButtons(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Expanded(child: Divider()), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - LocaleKeys.signIn_or.tr(), - style: TextStyle( - fontSize: 16, - color: theme.textColorScheme.secondary, - ), - ), - ), - 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, - ), - ], - ); - } - - 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(), - ], - ); - } -} 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 deleted file mode 100644 index b359b2e217..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ /dev/null @@ -1,44 +0,0 @@ -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_sign_in_screen.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class SignInScreen extends StatelessWidget { - const SignInScreen({super.key}); - - static const routeName = '/SignInScreen'; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(), - child: BlocConsumer( - listener: _showSignInError, - builder: (context, state) { - return UniversalPlatform.isDesktop - ? const DesktopSignInScreen() - : const MobileSignInScreen(); - }, - ), - ); - } - - 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 ec4fd1bbee..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,226 +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(); - - @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(), - ); - }); - } - }, - 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); - - 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 - 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); - } - }, - size: AFButtonSize.l, - alignment: Alignment.center, - ), - - spacing, - ]; - } - - 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 deleted file mode 100644 index 45e4fe7273..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ /dev/null @@ -1,133 +0,0 @@ -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: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}); - - @override - State createState() => - _SignInWithMagicLinkButtonsState(); -} - -class _SignInWithMagicLinkButtonsState - extends State { - final controller = TextEditingController(); - final FocusNode _focusNode = FocusNode(); - - @override - void dispose() { - controller.dispose(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: UniversalPlatform.isMobile ? 38.0 : 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), - _ConfirmButton( - onTap: () => _sendMagicLink(context, controller.text), - ), - ], - ); - } - - void _sendMagicLink(BuildContext context, String email) { - if (!isEmail(email)) { - showToastNotification( - message: LocaleKeys.signIn_invalidEmail.tr(), - type: ToastificationType.error, - ); - return; - } - - context - .read() - .add(SignInEvent.signInWithMagicLink(email: email)); - - showConfirmDialog( - context: context, - title: LocaleKeys.signIn_magicLinkSent.tr(), - description: LocaleKeys.signIn_magicLinkSentDescription.tr(), - ); - } -} - -class _ConfirmButton extends StatelessWidget { - const _ConfirmButton({ - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final name = switch (state.loginType) { - LoginType.signIn => LocaleKeys.signIn_signInWithMagicLink.tr(), - LoginType.signUp => LocaleKeys.signIn_signUpWithMagicLink.tr(), - }; - if (UniversalPlatform.isMobile) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 32), - maximumSize: const Size(double.infinity, 38), - ), - onPressed: onTap, - child: FlowyText( - name, - fontSize: 14, - color: Theme.of(context).colorScheme.onPrimary, - ), - ); - } else { - return SizedBox( - height: 48, - child: FlowyButton( - isSelected: true, - onTap: onTap, - hoverColor: Theme.of(context).colorScheme.primary, - 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 deleted file mode 100644 index 33ef1d7bb0..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart +++ /dev/null @@ -1,107 +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/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 SignInAnonymousButtonV2 extends StatelessWidget { - const SignInAnonymousButtonV2({ - 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 theme = AppFlowyTheme.of(context); - 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 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, - ), - size: AFButtonSize.s, - onTap: onTap, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.anonymous_mode_m, - color: theme.textColorScheme.secondary, - ); - }, - ); - }, - ), - ), - ); - }, - ); - } -} - -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 deleted file mode 100644 index 7067844500..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileLogoutButton extends StatelessWidget { - const MobileLogoutButton({ - super.key, - this.icon, - required this.text, - this.textColor, - required this.onPressed, - }); - - final FlowySvgData? icon; - final String text; - final Color? textColor; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return AFOutlinedIconTextButton.normal( - text: text, - onTap: onPressed, - size: AFButtonSize.l, - iconBuilder: (context, isHovering, disabled) { - if (icon == null) { - return const SizedBox.shrink(); - } - return FlowySvg( - icon!, - size: Size.square(18), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/switch_sign_in_sign_up_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/switch_sign_in_sign_up_button.dart deleted file mode 100644 index 662c1130fa..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/switch_sign_in_sign_up_button.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/sign_in_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 SwitchSignInSignUpButton extends StatelessWidget { - const SwitchSignInSignUpButton({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText( - switch (state.loginType) { - LoginType.signIn => - LocaleKeys.signIn_dontHaveAnAccount.tr(), - LoginType.signUp => - LocaleKeys.signIn_alreadyHaveAnAccount.tr(), - }, - fontSize: 12, - ), - const HSpace(4), - FlowyText( - switch (state.loginType) { - LoginType.signIn => LocaleKeys.signIn_createAccount.tr(), - LoginType.signUp => LocaleKeys.signIn_logIn.tr(), - }, - color: Colors.blue, - fontSize: 12, - ), - ], - ), - ), - ); - }, - ); - } -} 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/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart deleted file mode 100644 index 6d79b896c1..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart +++ /dev/null @@ -1,7 +0,0 @@ -export 'continue_with/continue_with_email_and_password.dart'; -export 'sign_in_agreement.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'; 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 deleted file mode 100644 index ee089dfce0..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart +++ /dev/null @@ -1,336 +0,0 @@ -import 'package:appflowy/core/frameless_window.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/startup/startup.dart'; -import 'package:appflowy/user/application/anon_user_bloc.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -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: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}); - - static const routeName = '/SkipLogInScreen'; - - @override - State createState() => _SkipLogInScreenState(); -} - -class _SkipLogInScreenState extends State { - var _didCustomizeFolder = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: const _SkipLoginMoveWindow(), - body: Center(child: _renderBody(context)), - ); - } - - Widget _renderBody(BuildContext context) { - final size = MediaQuery.of(context).size; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - FlowyLogoTitle( - title: LocaleKeys.welcomeText.tr(), - logoSize: Size.square(UniversalPlatform.isMobile ? 80 : 40), - ), - const VSpace(32), - GoButton( - onPressed: () { - if (_didCustomizeFolder) { - _relaunchAppAndAutoRegister(); - } else { - _autoRegister(context); - } - }, - ), - // if (Env.enableCustomCloud) ...[ - // const VSpace(10), - // const SizedBox( - // width: 340, - // child: _SetupYourServer(), - // ), - // ], - const VSpace(32), - SizedBox( - width: size.width * 0.7, - child: FolderWidget( - createFolderCallback: () async => _didCustomizeFolder = true, - ), - ), - const Spacer(), - const SkipLoginPageFooter(), - const VSpace(20), - ], - ); - } - - Future _autoRegister(BuildContext context) async { - final result = await getIt().signUpAsGuest(); - result.fold( - (user) => getIt().goHomeScreen(context, user), - (error) => Log.error(error), - ); - } - - Future _relaunchAppAndAutoRegister() async => runAppFlowy(isAnon: true); -} - -class SkipLoginPageFooter extends StatelessWidget { - const SkipLoginPageFooter({super.key}); - - @override - Widget build(BuildContext context) { - // The placeholderWidth should be greater than the longest width of the LanguageSelectorOnWelcomePage - const double placeholderWidth = 180; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!UniversalPlatform.isMobile) const HSpace(placeholderWidth), - const Expanded(child: SubscribeButtons()), - const SizedBox( - width: placeholderWidth, - height: 28, - child: Row( - children: [ - Spacer(), - LanguageSelectorOnWelcomePage(), - ], - ), - ), - ], - ), - ); - } -} - -class SubscribeButtons extends StatelessWidget { - const SubscribeButtons({super.key}); - - @override - Widget build(BuildContext context) { - return Wrap( - alignment: WrapAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText.regular( - LocaleKeys.youCanAlso.tr(), - fontSize: FontSizes.s12, - ), - FlowyTextButton( - LocaleKeys.githubStarText.tr(), - padding: const EdgeInsets.symmetric(horizontal: 4), - fontWeight: FontWeight.w500, - fontColor: Theme.of(context).colorScheme.primary, - hoverColor: Colors.transparent, - fillColor: Colors.transparent, - onPressed: () => - afLaunchUrlString('https://github.com/AppFlowy-IO/appflowy'), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText.regular(LocaleKeys.and.tr(), fontSize: FontSizes.s12), - FlowyTextButton( - LocaleKeys.subscribeNewsletterText.tr(), - padding: const EdgeInsets.symmetric(horizontal: 4.0), - fontWeight: FontWeight.w500, - fontColor: Theme.of(context).colorScheme.primary, - hoverColor: Colors.transparent, - fillColor: Colors.transparent, - onPressed: () => - afLaunchUrlString('https://www.appflowy.io/blog'), - ), - ], - ), - ], - ); - } -} - -class LanguageSelectorOnWelcomePage extends StatelessWidget { - const LanguageSelectorOnWelcomePage({super.key}); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - offset: const Offset(0, -450), - direction: PopoverDirection.bottomWithRightAligned, - child: FlowyButton( - useIntrinsicWidth: true, - text: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const FlowySvg(FlowySvgs.ethernet_m, size: Size.square(20)), - const HSpace(4), - Builder( - builder: (context) { - final currentLocale = - context.watch().state.locale; - return FlowyText(languageFromLocale(currentLocale)); - }, - ), - const FlowySvg(FlowySvgs.drop_menu_hide_m, size: Size.square(20)), - ], - ), - ), - popupBuilder: (BuildContext context) { - final easyLocalization = EasyLocalization.of(context); - if (easyLocalization == null) { - return const SizedBox.shrink(); - } - - return LanguageItemsListView( - allLocales: easyLocalization.supportedLocales, - ); - }, - ); - } -} - -class LanguageItemsListView extends StatelessWidget { - const LanguageItemsListView({super.key, required this.allLocales}); - - final List allLocales; - - @override - Widget build(BuildContext context) { - // get current locale from cubit - final state = context.watch().state; - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 400), - child: ListView.builder( - itemCount: allLocales.length, - itemBuilder: (context, index) { - final locale = allLocales[index]; - return LanguageItem(locale: locale, currentLocale: state.locale); - }, - ), - ); - } -} - -class LanguageItem extends StatelessWidget { - const LanguageItem({ - super.key, - required this.locale, - required this.currentLocale, - }); - - final Locale locale; - final Locale currentLocale; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 32, - child: FlowyButton( - text: FlowyText.medium( - languageFromLocale(locale), - ), - rightIcon: - currentLocale == locale ? const FlowySvg(FlowySvgs.check_s) : null, - onTap: () { - if (currentLocale != locale) { - context.read().setLocale(context, locale); - } - PopoverContainer.of(context).close(); - }, - ), - ); - } -} - -class GoButton extends StatelessWidget { - const GoButton({super.key, required this.onPressed}); - - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - 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.letsGoButtonText.tr() - : LocaleKeys.signIn_continueAnonymousUser.tr(); - - final textWidget = Row( - children: [ - Expanded( - child: FlowyText.medium( - text, - textAlign: TextAlign.center, - fontSize: 14, - ), - ), - ], - ); - - return SizedBox( - width: 340, - height: 48, - child: FlowyButton( - isSelected: true, - text: textWidget, - radius: Corners.s6Border, - onTap: () { - if (state.anonUsers.isNotEmpty) { - final bloc = context.read(); - final historicalUser = state.anonUsers.first; - bloc.add( - AnonUserEvent.openAnonUser(historicalUser), - ); - } else { - onPressed(); - } - }, - ), - ); - }, - ), - ), - ); - } -} - -class _SkipLoginMoveWindow extends StatelessWidget - implements PreferredSizeWidget { - const _SkipLoginMoveWindow(); - - @override - Widget build(BuildContext context) => - const Row(children: [Expanded(child: MoveWindowDetector())]); - - @override - Size get preferredSize => const Size.fromHeight(55.0); -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart deleted file mode 100644 index 4062cedf8e..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/splash_bloc.dart'; -import 'package:appflowy/user/domain/auth_state.dart'; -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: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. - const SplashScreen({super.key, required this.isAnon}); - - final bool isAnon; - - @override - Widget build(BuildContext context) { - if (isAnon) { - return FutureBuilder( - future: _registerIfNeeded(), - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const SizedBox.shrink(); - } - return _buildChild(context); - }, - ); - } else { - return _buildChild(context); - } - } - - BlocProvider _buildChild(BuildContext context) { - return BlocProvider( - create: (context) => - getIt()..add(const SplashEvent.getUser()), - child: Scaffold( - body: BlocListener( - listener: (context, state) { - state.auth.map( - authenticated: (r) => _handleAuthenticated(context, r), - unauthenticated: (r) => _handleUnauthenticated(context, r), - initial: (r) => {}, - ); - }, - child: const Body(), - ), - ), - ); - } - - /// Handles the authentication flow once a user is authenticated. - Future _handleAuthenticated( - 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, - ); - }, - (error) => handleOpenWorkspaceError(context, error), - ); - } - - void _handleUnauthenticated(BuildContext context, Unauthenticated result) { - // replace Splash screen as root page - if (isAuthEnabled || UniversalPlatform.isMobile) { - context.go(SignInScreen.routeName); - } else { - // if the env is not configured, we will skip to the 'skip login screen'. - context.go(SkipLogInScreen.routeName); - } - } - - Future _registerIfNeeded() async { - final result = await UserEventGetUserProfile().send(); - if (result.isFailure) { - await getIt().signUpAsGuest(); - } - } -} - -class Body extends StatelessWidget { - const Body({super.key}); - @override - Widget build(BuildContext context) { - return Container( - alignment: Alignment.center, - child: UniversalPlatform.isMobile - ? const FlowySvg(FlowySvgs.app_logo_xl, blendMode: null) - : const _DesktopSplashBody(), - ); - } -} - -class _DesktopSplashBody extends StatelessWidget { - const _DesktopSplashBody(); - - @override - Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; - return SingleChildScrollView( - child: Stack( - alignment: Alignment.center, - children: [ - Image( - fit: BoxFit.cover, - width: size.width, - height: size.height, - image: const AssetImage( - 'assets/images/appflowy_launch_splash.jpg', - ), - ), - const CircularProgressIndicator.adaptive(), - ], - ), - ); - } -} 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 deleted file mode 100644 index af6d4ad770..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ /dev/null @@ -1,157 +0,0 @@ -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_backend/protobuf/flowy-error/errors.pb.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/style_widget/snap_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../application/workspace_error_bloc.dart'; - -class WorkspaceErrorScreen extends StatelessWidget { - const WorkspaceErrorScreen({ - super.key, - required this.userFolder, - required this.error, - }); - - final UserFolderPB userFolder; - final FlowyError error; - - static const routeName = "/WorkspaceErrorScreen"; - // arguments names to used in GoRouter - static const argError = "error"; - static const argUserFolder = "userFolder"; - - @override - Widget build(BuildContext context) { - return Scaffold( - extendBody: true, - body: BlocProvider( - create: (context) => WorkspaceErrorBloc( - userFolder: userFolder, - error: error, - )..add(const WorkspaceErrorEvent.init()), - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (previous, current) => - previous.workspaceState != current.workspaceState, - listener: (context, state) async { - await state.workspaceState.when( - initial: () {}, - logout: () async { - await getIt().signOut(); - await runAppFlowy(); - }, - reset: () async { - await getIt().signOut(); - await runAppFlowy(); - }, - restoreFromSnapshot: () {}, - createNewWorkspace: () {}, - ); - }, - ), - BlocListener( - listenWhen: (previous, current) => - previous.loadingState != current.loadingState, - listener: (context, state) async { - state.loadingState?.when( - loading: () {}, - finish: (error) { - error.fold( - (_) {}, - (err) { - showSnapBar(context, err.msg); - }, - ); - }, - idle: () {}, - ); - }, - ), - ], - child: BlocBuilder( - builder: (context, state) { - final List children = [ - WorkspaceErrorDescription(error: error), - ]; - - children.addAll([ - const VSpace(50), - const LogoutButton(), - const VSpace(20), - ]); - - return Center( - child: SizedBox( - width: 500, - child: IntrinsicHeight( - child: Column( - children: children, - ), - ), - ), - ); - }, - ), - ), - ), - ); - } -} - -class WorkspaceErrorDescription extends StatelessWidget { - const WorkspaceErrorDescription({super.key, required this.error}); - - final FlowyError error; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - state.initialError.msg.toString(), - fontSize: 14, - maxLines: 10, - ), - FlowyText.medium( - "Error code: ${state.initialError.code.value.toString()}", - fontSize: 12, - ), - ], - ); - }, - ); - } -} - -class LogoutButton extends StatelessWidget { - const LogoutButton({super.key}); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 40, - width: 200, - child: FlowyButton( - text: FlowyText.medium( - LocaleKeys.settings_menu_logout.tr(), - textAlign: TextAlign.center, - ), - onTap: () async { - context.read().add( - const WorkspaceErrorEvent.logout(), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index af5e7367e5..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart +++ /dev/null @@ -1,107 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class DesktopWorkspaceStartScreen extends StatelessWidget { - const DesktopWorkspaceStartScreen({super.key, required this.workspaceState}); - - final WorkspaceState workspaceState; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Padding( - padding: const EdgeInsets.all(60.0), - child: Column( - children: [ - _renderBody(workspaceState), - _renderCreateButton(context), - ], - ), - ), - ); - } -} - -Widget _renderBody(WorkspaceState state) { - final body = state.successOrFailure.fold( - (_) => _renderList(state.workspaces), - (error) => Center( - child: AppFlowyErrorPage( - error: error, - ), - ), - ); - return body; -} - -Widget _renderList(List workspaces) { - return Expanded( - child: StyledListView( - itemBuilder: (BuildContext context, int index) { - final workspace = workspaces[index]; - return _WorkspaceItem( - workspace: workspace, - onPressed: (workspace) => _popToWorkspace(context, workspace), - ); - }, - itemCount: workspaces.length, - ), - ); -} - -class _WorkspaceItem extends StatelessWidget { - const _WorkspaceItem({ - required this.workspace, - required this.onPressed, - }); - - final WorkspacePB workspace; - final void Function(WorkspacePB workspace) onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 46, - child: FlowyTextButton( - workspace.name, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - fontSize: 14, - onPressed: () => onPressed(workspace), - ), - ); - } -} - -Widget _renderCreateButton(BuildContext context) { - return SizedBox( - width: 200, - height: 40, - child: FlowyTextButton( - LocaleKeys.workspace_create.tr(), - fontSize: 14, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onPressed: () { - // same method as in mobile - context.read().add( - WorkspaceEvent.createWorkspace( - LocaleKeys.workspace_hint.tr(), - "", - ), - ); - }, - ), - ); -} - -// same method as in mobile -void _popToWorkspace(BuildContext context, WorkspacePB workspace) { - context.pop(workspace.id); -} 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 deleted file mode 100644 index a6124da60b..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart +++ /dev/null @@ -1,149 +0,0 @@ -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:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -// TODO: needs refactor when multiple workspaces are supported -class MobileWorkspaceStartScreen extends StatefulWidget { - const MobileWorkspaceStartScreen({ - super.key, - required this.workspaceState, - }); - - @override - State createState() => - _MobileWorkspaceStartScreenState(); - final WorkspaceState workspaceState; -} - -class _MobileWorkspaceStartScreenState - extends State { - WorkspacePB? selectedWorkspace; - final TextEditingController controller = TextEditingController(); - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final style = Theme.of(context); - final size = MediaQuery.of(context).size; - const double spacing = 16.0; - final List> workspaceEntries = - >[]; - for (final WorkspacePB workspace in widget.workspaceState.workspaces) { - workspaceEntries.add( - DropdownMenuEntry( - value: workspace, - label: workspace.name, - ), - ); - } - -// render the workspace dropdown menu if success, otherwise render error page - final body = widget.workspaceState.successOrFailure.fold( - (_) { - return Padding( - padding: const EdgeInsets.fromLTRB(50, 0, 50, 30), - child: Column( - children: [ - const Spacer(), - const FlowySvg( - FlowySvgs.app_logo_xl, - size: Size.square(64), - blendMode: null, - ), - const VSpace(spacing * 2), - Text( - LocaleKeys.workspace_chooseWorkspace.tr(), - style: style.textTheme.displaySmall, - textAlign: TextAlign.center, - ), - const VSpace(spacing * 4), - DropdownMenu( - width: size.width - 100, - // TODO: The following code cause the bad state error, need to fix it - // initialSelection: widget.workspaceState.workspaces.first, - label: const Text('Workspace'), - controller: controller, - dropdownMenuEntries: workspaceEntries, - onSelected: (WorkspacePB? workspace) { - setState(() { - selectedWorkspace = workspace; - }); - }, - ), - const Spacer(), - // TODO: needs to implement create workspace in the future - // TextButton( - // child: Text( - // LocaleKeys.workspace_create.tr(), - // style: style.textTheme.labelMedium, - // textAlign: TextAlign.center, - // ), - // onPressed: () { - // setState(() { - // // same method as in desktop - // context.read().add( - // WorkspaceEvent.createWorkspace( - // LocaleKeys.workspace_hint.tr(), - // "", - // ), - // ); - // }); - // }, - // ), - const VSpace(spacing / 2), - ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 56), - ), - onPressed: () { - if (selectedWorkspace == null) { - // If user didn't choose any workspace, pop to the initial workspace(first workspace) - _popToWorkspace( - context, - widget.workspaceState.workspaces.first, - ); - return; - } - // pop to the selected workspace - _popToWorkspace( - context, - selectedWorkspace!, - ); - }, - child: Text(LocaleKeys.signUp_getStartedText.tr()), - ), - const VSpace(spacing), - ], - ), - ); - }, - (error) { - return Center( - child: AppFlowyErrorPage( - error: error, - ), - ); - }, - ); - - return Scaffold( - body: body, - ); - } -} - -// same method as in desktop -void _popToWorkspace(BuildContext context, WorkspacePB workspace) { - context.pop(workspace.id); -} 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 deleted file mode 100644 index a8a9305539..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart'; -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -// For future use -class WorkspaceStartScreen extends StatelessWidget { - /// To choose which screen is going to open - const WorkspaceStartScreen({super.key, required this.userProfile}); - - final UserProfilePB userProfile; - - static const routeName = "/WorkspaceStartScreen"; - static const argUserProfile = "userProfile"; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => getIt(param1: userProfile) - ..add(const WorkspaceEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - if (UniversalPlatform.isMobile) { - return MobileWorkspaceStartScreen( - workspaceState: state, - ); - } - return DesktopWorkspaceStartScreen( - workspaceState: state, - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart new file mode 100644 index 0000000000..59ace71f8c --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart @@ -0,0 +1,364 @@ +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/core/frameless_window.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/widgets/background.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/widget/rounded_button.dart'; +import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +class SignInScreen extends StatelessWidget { + const SignInScreen({ + super.key, + required this.router, + }); + + final AuthRouter router; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(), + child: BlocConsumer( + listener: (context, state) { + state.successOrFail.fold( + () => null, + (result) => _handleSuccessOrFail(result, context), + ); + }, + builder: (_, __) => Scaffold( + appBar: const PreferredSize( + preferredSize: Size(double.infinity, 60), + child: MoveWindowDetector(), + ), + body: SignInForm(router: router), + ), + ), + ); + } + + void _handleSuccessOrFail( + Either result, + BuildContext context, + ) { + result.fold( + (user) => router.pushHomeScreen(context, user), + (error) => showSnapBar(context, error.msg), + ); + } +} + +class SignInForm extends StatelessWidget { + const SignInForm({ + super.key, + required this.router, + }); + + final AuthRouter router; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: AuthFormContainer( + children: [ + // Email. + FlowyLogoTitle( + title: LocaleKeys.signIn_loginTitle.tr(), + logoSize: const Size(60, 60), + ), + const VSpace(30), + // Email and password. don't support yet. + /* + ...[ + const EmailTextField(), + const VSpace(5), + const PasswordTextField(), + const VSpace(20), + const LoginButton(), + const VSpace(10), + + const VSpace(10), + SignUpPrompt(router: router), + ], + */ + + const SignInAsGuestButton(), + + // third-party sign in. + const VSpace(20), + const OrContinueWith(), + const VSpace(10), + const ThirdPartySignInButtons(), + const VSpace(20), + + // loading status + if (context.read().state.isSubmitting) ...[ + const SizedBox(height: 8), + const LinearProgressIndicator(value: null), + const VSpace(20), + ], + ], + ), + ); + } +} + +class SignUpPrompt extends StatelessWidget { + const SignUpPrompt({ + Key? key, + required this.router, + }) : super(key: key); + + final AuthRouter router; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText.medium( + LocaleKeys.signIn_dontHaveAnAccount.tr(), + color: Theme.of(context).hintColor, + ), + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: () => router.pushSignUpScreen(context), + child: Text( + LocaleKeys.signUp_buttonText.tr(), + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ), + ForgetPasswordButton(router: router), + ], + ); + } +} + +class LoginButton extends StatelessWidget { + const LoginButton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + title: LocaleKeys.signIn_loginButtonText.tr(), + height: 48, + borderRadius: Corners.s10Border, + onPressed: () => context + .read() + .add(const SignInEvent.signedInWithUserEmailAndPassword()), + ); + } +} + +class SignInAsGuestButton extends StatelessWidget { + const SignInAsGuestButton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + title: LocaleKeys.signIn_loginAsGuestButtonText.tr(), + height: 48, + borderRadius: Corners.s6Border, + onPressed: () { + getIt().set(KVKeys.loginType, 'local'); + context.read().add(const SignInEvent.signedInAsGuest()); + }, + ); + } +} + +class ForgetPasswordButton extends StatelessWidget { + const ForgetPasswordButton({ + Key? key, + required this.router, + }) : super(key: key); + + final AuthRouter router; + + @override + Widget build(BuildContext context) { + return TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: () { + throw UnimplementedError(); + }, + child: Text( + LocaleKeys.signIn_forgotPassword.tr(), + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ); + } +} + +class PasswordTextField extends StatelessWidget { + const PasswordTextField({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.passwordError != current.passwordError, + builder: (context, state) { + return RoundedInputField( + obscureText: true, + obscureIcon: svgWidget("home/hide"), + obscureHideIcon: svgWidget("home/show"), + hintText: LocaleKeys.signIn_passwordHint.tr(), + errorText: context + .read() + .state + .passwordError + .fold(() => "", (error) => error), + onChanged: (value) => context + .read() + .add(SignInEvent.passwordChanged(value)), + ); + }, + ); + } +} + +class EmailTextField extends StatelessWidget { + const EmailTextField({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.emailError != current.emailError, + builder: (context, state) { + return RoundedInputField( + hintText: LocaleKeys.signIn_emailHint.tr(), + errorText: context + .read() + .state + .emailError + .fold(() => "", (error) => error), + onChanged: (value) => + context.read().add(SignInEvent.emailChanged(value)), + ); + }, + ); + } +} + +class OrContinueWith extends StatelessWidget { + const OrContinueWith({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Flexible( + child: Divider( + color: Colors.white, + height: 10, + ), + ), + FlowyText.regular(' Or continue with '), + Flexible( + child: Divider( + color: Colors.white, + height: 10, + ), + ), + ], + ); + } +} + +class ThirdPartySignInButton extends StatelessWidget { + const ThirdPartySignInButton({ + Key? key, + required this.icon, + required this.onPressed, + }) : super(key: key); + + final String icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + height: 48, + width: 48, + iconPadding: const EdgeInsets.all(8.0), + radius: Corners.s10Border, + onPressed: onPressed, + icon: svgWidget( + icon, + ), + ); + } +} + +class ThirdPartySignInButtons extends StatelessWidget { + const ThirdPartySignInButtons({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ThirdPartySignInButton( + icon: 'login/google-mark', + onPressed: () { + getIt().set(KVKeys.loginType, 'supabase'); + context + .read() + .add(const SignInEvent.signedInWithOAuth('google')); + }, + ), + const SizedBox(width: 20), + ThirdPartySignInButton( + icon: 'login/github-mark', + onPressed: () { + getIt().set(KVKeys.loginType, 'supabase'); + context + .read() + .add(const SignInEvent.signedInWithOAuth('github')); + }, + ), + const SizedBox(width: 20), + ThirdPartySignInButton( + icon: 'login/discord-mark', + onPressed: () { + getIt().set(KVKeys.loginType, 'supabase'); + context + .read() + .add(const SignInEvent.signedInWithOAuth('discord')); + }, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_up_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_up_screen.dart new file mode 100644 index 0000000000..95f9c2fef8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_up_screen.dart @@ -0,0 +1,232 @@ +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/background.dart'; +import 'package:easy_localization/easy_localization.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:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +class SignUpScreen extends StatelessWidget { + const SignUpScreen({ + super.key, + required this.router, + }); + + final AuthRouter router; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(), + child: BlocListener( + listener: (context, state) { + state.successOrFail.fold( + () => {}, + (result) => _handleSuccessOrFail(context, result), + ); + }, + child: const Scaffold(body: SignUpForm()), + ), + ); + } + + void _handleSuccessOrFail( + BuildContext context, + Either result, + ) { + result.fold( + (user) => router.pushWelcomeScreen(context, user), + (error) => showSnapBar(context, error.msg), + ); + } +} + +class SignUpForm extends StatelessWidget { + const SignUpForm({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + 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(value: null), + ] + ], + ), + ); + } +} + +class SignUpPrompt extends StatelessWidget { + const SignUpPrompt({ + Key? key, + }) : super(key: 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({ + Key? key, + }) : super(key: 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({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.passwordError != current.passwordError, + builder: (context, state) { + return RoundedInputField( + obscureText: true, + obscureIcon: svgWidget("home/hide"), + obscureHideIcon: svgWidget("home/show"), + 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 + .fold(() => "", (error) => error), + onChanged: (value) => context + .read() + .add(SignUpEvent.passwordChanged(value)), + ); + }, + ); + } +} + +class RepeatPasswordTextField extends StatelessWidget { + const RepeatPasswordTextField({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.repeatPasswordError != current.repeatPasswordError, + builder: (context, state) { + return RoundedInputField( + obscureText: true, + obscureIcon: svgWidget("home/hide"), + obscureHideIcon: svgWidget("home/show"), + 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 + .fold(() => "", (error) => error), + onChanged: (value) => context + .read() + .add(SignUpEvent.repeatPasswordChanged(value)), + ); + }, + ); + } +} + +class EmailTextField extends StatelessWidget { + const EmailTextField({ + Key? key, + }) : super(key: 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 + .fold(() => "", (error) => error), + onChanged: (value) => + context.read().add(SignUpEvent.emailChanged(value)), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart new file mode 100644 index 0000000000..ee4e17ddbe --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart @@ -0,0 +1,316 @@ +import 'package:appflowy/core/frameless_window.dart'; +import 'package:appflowy/startup/entry_point.dart'; +import 'package:appflowy/startup/launch_configuration.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/appearance.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/language.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.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-folder2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../generated/locale_keys.g.dart'; +import 'folder/folder_widget.dart'; +import 'router.dart'; +import 'widgets/background.dart'; + +class SkipLogInScreen extends StatefulWidget { + final AuthRouter router; + final AuthService authService; + + const SkipLogInScreen({ + Key? key, + required this.router, + required this.authService, + }) : super(key: key); + + @override + State createState() => _SkipLogInScreenState(); +} + +class _SkipLogInScreenState extends State { + var _didCustomizeFolder = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const _SkipLoginMoveWindow(), + body: Center( + child: _renderBody(context), + ), + ); + } + + Widget _renderBody(BuildContext context) { + final size = MediaQuery.of(context).size; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + FlowyLogoTitle( + title: LocaleKeys.welcomeText.tr(), + logoSize: const Size.square(40), + ), + const VSpace(32), + GoButton( + onPressed: () { + if (_didCustomizeFolder) { + _relaunchAppAndAutoRegister(); + } else { + _autoRegister(context); + } + }, + ), + const VSpace(32), + SizedBox( + width: size.width * 0.5, + child: FolderWidget( + createFolderCallback: () async { + _didCustomizeFolder = true; + }, + ), + ), + const Spacer(), + const VSpace(48), + const SkipLoginPageFooter(), + const VSpace(20), + ], + ); + } + + Future _autoRegister(BuildContext context) async { + final result = await widget.authService.signUpAsGuest(); + result.fold( + (error) { + Log.error(error); + }, + (user) { + FolderEventGetCurrentWorkspace().send().then((result) { + _openCurrentWorkspace(context, user, result); + }); + }, + ); + } + + Future _relaunchAppAndAutoRegister() async { + await FlowyRunner.run( + FlowyApp(), + integrationEnv(), + config: const LaunchConfiguration( + autoRegistrationSupported: true, + ), + ); + } + + void _openCurrentWorkspace( + BuildContext context, + UserProfilePB user, + dartz.Either workspacesOrError, + ) { + workspacesOrError.fold( + (workspaceSetting) { + widget.router + .pushHomeScreenWithWorkSpace(context, user, workspaceSetting); + }, + (error) { + Log.error(error); + }, + ); + } +} + +class SkipLoginPageFooter extends StatelessWidget { + const SkipLoginPageFooter({ + super.key, + }); + + @override + Widget build(BuildContext context) { + // The placeholderWidth should be greater than the longest width of the LanguageSelectorOnWelcomePage + const double placeholderWidth = 180; + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HSpace(placeholderWidth), + Expanded(child: SubscribeButtons()), + SizedBox( + width: placeholderWidth, + height: 28, + child: Row( + children: [ + Spacer(), + LanguageSelectorOnWelcomePage(), + ], + ), + ), + ], + ), + ); + } +} + +class SubscribeButtons extends StatelessWidget { + const SubscribeButtons({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText.regular( + LocaleKeys.youCanAlso.tr(), + fontSize: FontSizes.s12, + ), + FlowyTextButton( + LocaleKeys.githubStarText.tr(), + fontWeight: FontWeight.w500, + fontColor: Theme.of(context).colorScheme.primary, + hoverColor: Colors.transparent, + fillColor: Colors.transparent, + onPressed: () => _launchURL( + 'https://github.com/AppFlowy-IO/appflowy', + ), + ), + FlowyText.regular( + LocaleKeys.and.tr(), + fontSize: FontSizes.s12, + ), + FlowyTextButton( + LocaleKeys.subscribeNewsletterText.tr(), + overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w500, + fontColor: Theme.of(context).colorScheme.primary, + hoverColor: Colors.transparent, + fillColor: Colors.transparent, + onPressed: () => _launchURL('https://www.appflowy.io/blog'), + ), + ], + ); + } + + Future _launchURL(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + throw 'Could not launch $url'; + } + } +} + +class LanguageSelectorOnWelcomePage extends StatelessWidget { + const LanguageSelectorOnWelcomePage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + offset: const Offset(0, -450), + direction: PopoverDirection.bottomWithRightAligned, + child: FlowyButton( + useIntrinsicWidth: true, + text: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const FlowySvg( + name: 'login/language', + size: Size.square(20), + ), + const HSpace(4), + FlowyText( + languageFromLocale(state.locale), + ), + // const HSpace(4), + const FlowySvg( + name: 'home/drop_down_hide', + size: Size.square(20), + ), + ], + ), + ), + popupBuilder: (BuildContext context) { + final easyLocalization = EasyLocalization.of(context); + if (easyLocalization == null) { + return const SizedBox.shrink(); + } + final allLocales = easyLocalization.supportedLocales; + return LanguageItemsListView( + allLocales: allLocales, + ); + }, + ); + }, + ); + } +} + +class GoButton extends StatelessWidget { + final VoidCallback onPressed; + + const GoButton({ + super.key, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return FlowyTextButton( + LocaleKeys.letsGoButtonText.tr(), + constraints: const BoxConstraints( + maxWidth: 340, + maxHeight: 48, + ), + radius: BorderRadius.circular(12), + mainAxisAlignment: MainAxisAlignment.center, + fontSize: FontSizes.s14, + fontFamily: GoogleFonts.poppins(fontWeight: FontWeight.w500).fontFamily, + padding: const EdgeInsets.symmetric(vertical: 14.0), + onPressed: onPressed, + fillColor: Theme.of(context).colorScheme.primary, + fontColor: Theme.of(context).colorScheme.onPrimary, + hoverColor: Theme.of(context).colorScheme.primaryContainer, + ); + } +} + +class _SkipLoginMoveWindow extends StatelessWidget + implements PreferredSizeWidget { + const _SkipLoginMoveWindow(); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + Expanded( + child: MoveWindowDetector(), + ), + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(55.0); +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart new file mode 100644 index 0000000000..74870d445a --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart @@ -0,0 +1,131 @@ +import 'package:appflowy/startup/tasks/supabase_task.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../startup/startup.dart'; +import '../application/splash_bloc.dart'; +import '../domain/auth_state.dart'; +import 'router.dart'; + +// [[diagram: splash screen]] +// ┌────────────────┐1.get user ┌──────────┐ ┌────────────┐ 2.send UserEventCheckUser +// │ SplashScreen │──────────▶│SplashBloc│────▶│ISplashUser │─────┐ +// └────────────────┘ └──────────┘ └────────────┘ │ +// │ +// ▼ +// ┌───────────┐ ┌─────────────┐ ┌────────┐ +// │HomeScreen │◀───────────│BlocListener │◀────────────────│RustSDK │ +// └───────────┘ └─────────────┘ └────────┘ +// 4. Show HomeScreen or SignIn 3.return AuthState +class SplashScreen extends StatelessWidget { + const SplashScreen({ + Key? key, + required this.autoRegister, + }) : super(key: key); + + final bool autoRegister; + + @override + Widget build(BuildContext context) { + if (!autoRegister) { + return _buildChild(context); + } else { + return FutureBuilder( + future: _registerIfNeeded(), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return Container(); + } + return _buildChild(context); + }, + ); + } + } + + BlocProvider _buildChild(BuildContext context) { + return BlocProvider( + create: (context) { + return getIt()..add(const SplashEvent.getUser()); + }, + child: Scaffold( + body: BlocListener( + listener: (context, state) { + state.auth.map( + authenticated: (r) => _handleAuthenticated(context, r), + unauthenticated: (r) => _handleUnauthenticated(context, r), + initial: (r) => {}, + ); + }, + child: const Body(), + ), + ), + ); + } + + Future _handleAuthenticated( + BuildContext context, + Authenticated authenticated, + ) async { + final userProfile = authenticated.userProfile; + final result = await FolderEventGetCurrentWorkspace().send(); + result.fold( + (workspaceSetting) { + getIt().pushHomeScreen( + context, + userProfile, + workspaceSetting, + ); + }, + (error) async { + Log.error(error); + getIt().pushWelcomeScreen(context, userProfile); + }, + ); + } + + void _handleUnauthenticated(BuildContext context, Unauthenticated result) { + // if the env is not configured, we will skip to the 'skip login screen'. + if (isSupabaseEnable) { + getIt().pushSignInScreen(context); + } else { + getIt().pushSkipLoginScreen(context); + } + } + + Future _registerIfNeeded() async { + final result = await UserEventCheckUser().send(); + if (!result.isLeft()) { + await getIt().signUpAsGuest(); + } + } +} + +class Body extends StatelessWidget { + const Body({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Container( + alignment: Alignment.center, + child: SingleChildScrollView( + child: Stack( + alignment: Alignment.center, + children: [ + Image( + fit: BoxFit.cover, + width: size.width, + height: size.height, + image: + const AssetImage('assets/images/appflowy_launch_splash.jpg'), + ), + const CircularProgressIndicator.adaptive(), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/welcome_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/welcome_screen.dart new file mode 100644 index 0000000000..b8b1361fad --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/welcome_screen.dart @@ -0,0 +1,115 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/workspace/welcome_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +class WelcomeScreen extends StatelessWidget { + final UserProfilePB userProfile; + const WelcomeScreen({ + Key? key, + required this.userProfile, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => getIt(param1: userProfile) + ..add(const WelcomeEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(60.0), + child: Column( + children: [ + _renderBody(state), + _renderCreateButton(context), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _renderBody(WelcomeState state) { + final body = state.successOrFailure.fold( + (_) => _renderList(state.workspaces), + (error) => FlowyErrorPage.message(error.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),), + ); + return body; + } + + Widget _renderCreateButton(BuildContext context) { + return SizedBox( + width: 200, + height: 40, + child: FlowyTextButton( + LocaleKeys.workspace_create.tr(), + fontSize: 14, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onPressed: () { + context.read().add( + WelcomeEvent.createWorkspace( + LocaleKeys.workspace_hint.tr(), + "", + ), + ); + }, + ), + ); + } + + Widget _renderList(List workspaces) { + return Expanded( + child: StyledListView( + itemBuilder: (BuildContext context, int index) { + final workspace = workspaces[index]; + return WorkspaceItem( + workspace: workspace, + onPressed: (workspace) => _handleOnPress(context, workspace), + ); + }, + itemCount: workspaces.length, + ), + ); + } + + void _handleOnPress(BuildContext context, WorkspacePB workspace) { + context.read().add(WelcomeEvent.openWorkspace(workspace)); + + Navigator.of(context).pop(workspace.id); + } +} + +class WorkspaceItem extends StatelessWidget { + final WorkspacePB workspace; + final void Function(WorkspacePB workspace) onPressed; + const WorkspaceItem({ + Key? key, + required this.workspace, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 46, + child: FlowyTextButton( + workspace.name, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + fontSize: 14, + onPressed: () => onPressed(workspace), + ), + ); + } +} 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 deleted file mode 100644 index c0b8e7e5ae..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class AuthFormContainer extends StatelessWidget { - const AuthFormContainer({ - super.key, - required this.children, - }); - - final List children; - - static const double width = 320; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: width, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: children, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/background.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/background.dart new file mode 100644 index 0000000000..779e7cf0ba --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/background.dart @@ -0,0 +1,61 @@ +import 'dart:math'; + +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.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:google_fonts/google_fonts.dart'; + +class AuthFormContainer extends StatelessWidget { + final List children; + const AuthFormContainer({ + Key? key, + required this.children, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return SizedBox( + width: min(size.width, 340), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ); + } +} + +class FlowyLogoTitle extends StatelessWidget { + final String title; + final Size logoSize; + const FlowyLogoTitle({ + Key? key, + required this.title, + this.logoSize = const Size.square(40), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.fromSize( + size: logoSize, + child: svgWidget('flowy_logo'), + ), + const VSpace(40), + 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/user/presentation/widgets/flowy_logo_title.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart deleted file mode 100644 index 14b1c896a9..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class FlowyLogoTitle extends StatelessWidget { - const FlowyLogoTitle({ - super.key, - required this.title, - this.logoSize = const Size.square(40), - }); - - final String title; - final Size logoSize; - - @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, - ), - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart deleted file mode 100644 index 64ed445be9..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart +++ /dev/null @@ -1,319 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/application/settings/prelude.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_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:google_fonts/google_fonts.dart'; - -import '../../../generated/locale_keys.g.dart'; -import '../../../startup/startup.dart'; -import '../../../workspace/presentation/home/toast.dart'; - -enum _FolderPage { - options, - create, - open, -} - -class FolderWidget extends StatefulWidget { - const FolderWidget({ - super.key, - required this.createFolderCallback, - }); - - final Future Function() createFolderCallback; - - @override - State createState() => _FolderWidgetState(); -} - -class _FolderWidgetState extends State { - var page = _FolderPage.options; - - @override - Widget build(BuildContext context) { - return _mapIndexToWidget(context); - } - - Widget _mapIndexToWidget(BuildContext context) { - switch (page) { - case _FolderPage.options: - return FolderOptionsWidget( - onPressedOpen: () { - _openFolder(); - }, - ); - case _FolderPage.create: - return CreateFolderWidget( - onPressedBack: () { - setState(() => page = _FolderPage.options); - }, - onPressedCreate: widget.createFolderCallback, - ); - case _FolderPage.open: - return const SizedBox.shrink(); - } - } - - Future _openFolder() async { - final path = await getIt().getDirectoryPath(); - if (path != null) { - await getIt().setCustomPath(path); - await widget.createFolderCallback(); - setState(() {}); - } - } -} - -class FolderOptionsWidget extends StatelessWidget { - const FolderOptionsWidget({ - super.key, - required this.onPressedOpen, - }); - - final VoidCallback onPressedOpen; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: getIt().getPath(), - builder: (context, result) { - final subtitle = result.hasData ? result.data! : ''; - return _FolderCard( - icon: const FlowySvg(FlowySvgs.archive_m), - title: LocaleKeys.settings_files_defineWhereYourDataIsStored.tr(), - subtitle: subtitle, - trailing: _buildTextButton( - context, - LocaleKeys.settings_files_set.tr(), - onPressedOpen, - ), - ); - }, - ); - } -} - -class CreateFolderWidget extends StatefulWidget { - const CreateFolderWidget({ - super.key, - required this.onPressedBack, - required this.onPressedCreate, - }); - - final VoidCallback onPressedBack; - final Future Function() onPressedCreate; - - @override - State createState() => CreateFolderWidgetState(); -} - -@visibleForTesting -class CreateFolderWidgetState extends State { - var _folderName = 'appflowy'; - @visibleForTesting - var directory = ''; - - final _fToast = FToast(); - - @override - void initState() { - super.initState(); - _fToast.init(context); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Align( - alignment: Alignment.centerLeft, - child: TextButton.icon( - onPressed: widget.onPressedBack, - icon: const Icon(Icons.arrow_back_rounded), - label: const Text('Back'), - ), - ), - _FolderCard( - title: LocaleKeys.settings_files_location.tr(), - subtitle: LocaleKeys.settings_files_locationDesc.tr(), - trailing: SizedBox( - width: 120, - child: FlowyTextField( - hintText: LocaleKeys.settings_files_folderHintText.tr(), - onChanged: (name) => _folderName = name, - onSubmitted: (name) => setState( - () => _folderName = name, - ), - ), - ), - ), - _FolderCard( - title: LocaleKeys.settings_files_folderPath.tr(), - subtitle: _path, - trailing: _buildTextButton( - context, - LocaleKeys.settings_files_browser.tr(), - () async { - final dir = await getIt().getDirectoryPath(); - if (dir != null) { - setState(() => directory = dir); - } - }, - ), - ), - const VSpace(4.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: _buildTextButton( - context, - LocaleKeys.settings_files_create.tr(), - () async { - if (_path.isEmpty) { - _showToast( - LocaleKeys.settings_files_locationCannotBeEmpty.tr(), - ); - } else { - await getIt().setCustomPath(_path); - await widget.onPressedCreate(); - } - }, - ), - ), - ], - ); - } - - String get _path { - if (directory.isEmpty) return ''; - final String path; - if (Platform.isMacOS) { - path = directory.replaceAll('/Volumes/Macintosh HD', ''); - } else { - path = directory; - } - return '$path/$_folderName'; - } - - void _showToast(String message) { - _fToast.showToast( - child: FlowyMessageToast(message: message), - gravity: ToastGravity.CENTER, - ); - } -} - -Widget _buildTextButton( - BuildContext context, - String title, - VoidCallback onPressed, -) { - return SecondaryTextButton( - title, - mode: TextButtonMode.small, - onPressed: onPressed, - ); -} - -class _FolderCard extends StatelessWidget { - const _FolderCard({ - required this.title, - required this.subtitle, - this.trailing, - this.icon, - }); - - final String title; - final String subtitle; - final Widget? icon; - final Widget? trailing; - - @override - Widget build(BuildContext context) { - const cardSpacing = 16.0; - return Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: cardSpacing, - horizontal: cardSpacing, - ), - child: Row( - children: [ - if (icon != null) - Padding( - padding: const EdgeInsets.only(right: cardSpacing), - child: icon!, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Flexible( - child: FlowyText.regular( - title, - fontSize: FontSizes.s14, - fontFamily: GoogleFonts.poppins( - fontWeight: FontWeight.w500, - ).fontFamily, - overflow: TextOverflow.ellipsis, - ), - ), - Tooltip( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(6), - ), - preferBelow: false, - richMessage: WidgetSpan( - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.alphabetic, - child: Container( - color: Theme.of(context).colorScheme.surface, - padding: const EdgeInsets.all(10), - constraints: const BoxConstraints(maxWidth: 450), - child: FlowyText( - LocaleKeys.settings_menu_customPathPrompt.tr(), - maxLines: null, - ), - ), - ), - child: const FlowyIconButton( - icon: Icon( - Icons.warning_amber_rounded, - size: 20, - color: Colors.orangeAccent, - ), - ), - ), - ], - ), - const VSpace(4), - FlowyText.regular( - subtitle, - overflow: TextOverflow.ellipsis, - fontSize: FontSizes.s12, - fontFamily: GoogleFonts.poppins( - fontWeight: FontWeight.w300, - ).fontFamily, - ), - ], - ), - ), - if (trailing != null) ...[ - const HSpace(cardSpacing), - trailing!, - ], - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/widgets.dart deleted file mode 100644 index eecb9d5300..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/widgets.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'folder_widget.dart'; -export 'flowy_logo_title.dart'; -export 'auth_form_container.dart'; diff --git a/frontend/appflowy_flutter/lib/util/base64_string.dart b/frontend/appflowy_flutter/lib/util/base64_string.dart new file mode 100644 index 0000000000..01a7f9a17e --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/base64_string.dart @@ -0,0 +1,5 @@ +import 'dart:convert'; + +extension Base64Encode on String { + String get base64 => base64Encode(utf8.encode(this)); +} diff --git a/frontend/appflowy_flutter/lib/util/built_in_svgs.dart b/frontend/appflowy_flutter/lib/util/built_in_svgs.dart deleted file mode 100644 index 6e7f2087b4..0000000000 --- a/frontend/appflowy_flutter/lib/util/built_in_svgs.dart +++ /dev/null @@ -1,13 +0,0 @@ -final builtInSVGIcons = [ - '1F9CC', - '1F9DB', - '1F9DD-200D-2642-FE0F', - '1F9DE-200D-2642-FE0F', - '1F9DF', - '1F42F', - '1F43A', - '1F431', - '1F435', - '1F600', - '1F984', -]; 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..f6d42101b6 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,11 @@ -import 'package:flutter/material.dart'; +import 'dart:ui'; -// 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]; +class ColorGenerator { + Color generateColorFromString(String string) { + final hash = string.hashCode; + final int r = (hash & 0xFF0000) >> 16; + final int g = (hash & 0x00FF00) >> 8; + final int b = hash & 0x0000FF; + return Color.fromRGBO(r, g, b, 0.5); } } diff --git a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart b/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart deleted file mode 100644 index 61694367bb..0000000000 --- a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:math' as math; - -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 a random color - static Color random({double opacity = 1.0}) { - return Color((math.Random().nextDouble() * 0xFFFFFF).toInt()) - .withValues(alpha: opacity); - } -} diff --git a/frontend/appflowy_flutter/lib/util/debounce.dart b/frontend/appflowy_flutter/lib/util/debounce.dart index 1929d07328..324818a650 100644 --- a/frontend/appflowy_flutter/lib/util/debounce.dart +++ b/frontend/appflowy_flutter/lib/util/debounce.dart @@ -1,16 +1,17 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + class Debounce { + final Duration duration; + Timer? _timer; + Debounce({ this.duration = const Duration(milliseconds: 1000), }); - final Duration duration; - Timer? _timer; - - void call(Function action) { + void call(VoidCallback action) { dispose(); - _timer = Timer(duration, () { action(); }); 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/either_extension.dart b/frontend/appflowy_flutter/lib/util/either_extension.dart new file mode 100644 index 0000000000..7ec1beeb53 --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/either_extension.dart @@ -0,0 +1,6 @@ +import 'package:dartz/dartz.dart'; + +extension EitherX on Either { + R asRight() => (this as Right).value; + L asLeft() => (this as Left).value; +} 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 deleted file mode 100644 index 4ef11bf5c6..0000000000 --- a/frontend/appflowy_flutter/lib/util/field_type_extension.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'dart:ui'; - -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) { - FieldType.RichText => LocaleKeys.grid_field_textFieldName.tr(), - FieldType.Number => LocaleKeys.grid_field_numberFieldName.tr(), - FieldType.DateTime => LocaleKeys.grid_field_dateFieldName.tr(), - FieldType.SingleSelect => - LocaleKeys.grid_field_singleSelectFieldName.tr(), - FieldType.MultiSelect => - LocaleKeys.grid_field_multiSelectFieldName.tr(), - FieldType.Checkbox => LocaleKeys.grid_field_checkboxFieldName.tr(), - FieldType.Checklist => LocaleKeys.grid_field_checklistFieldName.tr(), - FieldType.URL => LocaleKeys.grid_field_urlFieldName.tr(), - FieldType.LastEditedTime => - LocaleKeys.grid_field_updatedAtFieldName.tr(), - FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(), - FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(), - 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(), - }; - - FlowySvgData get svgData => switch (this) { - FieldType.RichText => FlowySvgs.text_s, - FieldType.Number => FlowySvgs.number_s, - FieldType.DateTime => FlowySvgs.date_s, - FieldType.SingleSelect => FlowySvgs.single_select_s, - FieldType.MultiSelect => FlowySvgs.multiselect_s, - 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.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(), - }; - - FlowySvgData? get rightIcon => switch (this) { - FieldType.Summary => FlowySvgs.ai_indicator_s, - FieldType.Translate => FlowySvgs.ai_indicator_s, - _ => null, - }; - - Color get mobileIconBackgroundColor => switch (this) { - FieldType.RichText => const Color(0xFFBECCFF), - FieldType.Number => const Color(0xFFCABDFF), - FieldType.URL => const Color(0xFFFFB9EF), - FieldType.SingleSelect => const Color(0xFFBECCFF), - FieldType.MultiSelect => const Color(0xFFBECCFF), - FieldType.DateTime => const Color(0xFFFDEDA7), - FieldType.LastEditedTime => const Color(0xFFFDEDA7), - FieldType.CreatedTime => const Color(0xFFFDEDA7), - FieldType.Checkbox => const Color(0xFF98F4CD), - FieldType.Checklist => const Color(0xFF98F4CD), - FieldType.Relation => const Color(0xFFFDEDA7), - FieldType.Summary => const Color(0xFFBECCFF), - FieldType.Time => const Color(0xFFFDEDA7), - FieldType.Translate => const Color(0xFFBECCFF), - FieldType.Media => const Color(0xFF91EBF5), - _ => throw UnimplementedError(), - }; - - // TODO(RS): inner icon color isn't always white - Color get mobileIconBackgroundColorDark => switch (this) { - FieldType.RichText => const Color(0xFF6859A7), - FieldType.Number => const Color(0xFF6859A7), - FieldType.URL => const Color(0xFFA75C96), - FieldType.SingleSelect => const Color(0xFF5366AB), - FieldType.MultiSelect => const Color(0xFF5366AB), - FieldType.DateTime => const Color(0xFFB0A26D), - FieldType.LastEditedTime => const Color(0xFFB0A26D), - FieldType.CreatedTime => const Color(0xFFB0A26D), - FieldType.Checkbox => const Color(0xFF42AD93), - FieldType.Checklist => const Color(0xFF42AD93), - FieldType.Relation => const Color(0xFFFDEDA7), - 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/file_extension.dart b/frontend/appflowy_flutter/lib/util/file_extension.dart deleted file mode 100644 index 69c20d1dcb..0000000000 --- a/frontend/appflowy_flutter/lib/util/file_extension.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'dart:io'; - -extension FileSizeExtension on String { - int? get fileSize { - final file = File(this); - if (file.existsSync()) { - return file.lengthSync(); - } - return null; - } -} diff --git a/frontend/appflowy_flutter/lib/util/file_picker/file_picker_impl.dart b/frontend/appflowy_flutter/lib/util/file_picker/file_picker_impl.dart new file mode 100644 index 0000000000..b6f16d6d4f --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/file_picker/file_picker_impl.dart @@ -0,0 +1,56 @@ +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:file_picker/file_picker.dart' as fp; + +class FilePicker implements FilePickerService { + @override + Future getDirectoryPath({String? title}) { + return fp.FilePicker.platform.getDirectoryPath(); + } + + @override + Future pickFiles({ + String? dialogTitle, + String? initialDirectory, + fp.FileType type = fp.FileType.any, + List? allowedExtensions, + Function(fp.FilePickerStatus p1)? onFileLoading, + bool allowCompression = true, + bool allowMultiple = false, + bool withData = false, + bool withReadStream = false, + bool lockParentWindow = false, + }) async { + final result = await fp.FilePicker.platform.pickFiles( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + onFileLoading: onFileLoading, + allowCompression: allowCompression, + allowMultiple: allowMultiple, + withData: withData, + withReadStream: withReadStream, + lockParentWindow: lockParentWindow, + ); + return FilePickerResult(result?.files ?? []); + } + + @override + Future saveFile({ + String? dialogTitle, + String? fileName, + String? initialDirectory, + fp.FileType type = fp.FileType.any, + List? allowedExtensions, + bool lockParentWindow = false, + }) { + return fp.FilePicker.platform.saveFile( + dialogTitle: dialogTitle, + fileName: fileName, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + lockParentWindow: lockParentWindow, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart b/frontend/appflowy_flutter/lib/util/file_picker/file_picker_service.dart similarity index 92% rename from frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart rename to frontend/appflowy_flutter/lib/util/file_picker/file_picker_service.dart index e8991397a9..66dec35d3b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart +++ b/frontend/appflowy_flutter/lib/util/file_picker/file_picker_service.dart @@ -1,8 +1,5 @@ import 'package:file_picker/file_picker.dart'; -export 'package:file_picker/file_picker.dart' - show FileType, FilePickerStatus, PlatformFile; - class FilePickerResult { const FilePickerResult(this.files); diff --git a/frontend/appflowy_flutter/lib/util/font_family_extension.dart b/frontend/appflowy_flutter/lib/util/font_family_extension.dart deleted file mode 100644 index 12fb5aaad0..0000000000 --- a/frontend/appflowy_flutter/lib/util/font_family_extension.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension FontFamilyExtension on String { - String parseFontFamilyName() => replaceAll('_regular', '') - .replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}'); - - // display the default font name if the font family name is empty - // or using the default font family - String get fontFamilyDisplayName => isEmpty || this == defaultFontFamily - ? LocaleKeys.settings_appearance_fontFamily_defaultFont.tr() - : parseFontFamilyName(); -} diff --git a/frontend/appflowy_flutter/lib/util/int64_extension.dart b/frontend/appflowy_flutter/lib/util/int64_extension.dart deleted file mode 100644 index 8c7f1579f5..0000000000 --- a/frontend/appflowy_flutter/lib/util/int64_extension.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:fixnum/fixnum.dart'; - -extension DateConversion on Int64 { - DateTime toDateTime() => DateTime.fromMillisecondsSinceEpoch(toInt() * 1000); -} diff --git a/frontend/appflowy_flutter/lib/util/json_print.dart b/frontend/appflowy_flutter/lib/util/json_print.dart index 35824b8212..0becb0e789 100644 --- a/frontend/appflowy_flutter/lib/util/json_print.dart +++ b/frontend/appflowy_flutter/lib/util/json_print.dart @@ -1,10 +1,6 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:flutter/material.dart'; - -const JsonEncoder _encoder = JsonEncoder.withIndent(' '); +// import 'dart:convert'; +// import 'package:appflowy_backend/log.dart'; +// const JsonEncoder _encoder = JsonEncoder.withIndent(' '); void prettyPrintJson(Object? object) { - Log.trace(_encoder.convert(object)); - debugPrint(_encoder.convert(object)); + // Log.trace(_encoder.convert(object)); } diff --git a/frontend/appflowy_flutter/lib/util/levenshtein.dart b/frontend/appflowy_flutter/lib/util/levenshtein.dart deleted file mode 100644 index b8b8eafecc..0000000000 --- a/frontend/appflowy_flutter/lib/util/levenshtein.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:math'; - -int levenshtein(String s, String t, {bool caseSensitive = true}) { - if (!caseSensitive) { - s = s.toLowerCase(); - t = t.toLowerCase(); - } - - if (s == t) return 0; - - final v0 = List.generate(t.length + 1, (i) => i); - final v1 = List.filled(t.length + 1, 0); - - for (var i = 0; i < s.length; i++) { - v1[0] = i + 1; - - for (var j = 0; j < t.length; j++) { - final cost = (s[i] == t[j]) ? 0 : 1; - v1[j + 1] = min(v1[j] + 1, min(v0[j + 1] + 1, v0[j] + cost)); - } - - v0.setAll(0, v1); - } - - return v1[t.length]; -} 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 deleted file mode 100644 index b8dd390627..0000000000 --- a/frontend/appflowy_flutter/lib/util/share_log_files.dart +++ /dev/null @@ -1,78 +0,0 @@ -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:archive/archive_io.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; - -Future shareLogFiles(BuildContext? context) async { - final dir = await getApplicationSupportDirectory(); - final zipEncoder = ZipEncoder(); - - final archiveLogFiles = dir - .listSync(recursive: true) - .where((e) => p.basename(e.path).startsWith('log.')) - .map((e) { - final bytes = File(e.path).readAsBytesSync(); - return ArchiveFile(p.basename(e.path), bytes.length, bytes); - }); - - if (archiveLogFiles.isEmpty) { - if (context != null && context.mounted) { - showToastNotification( - message: LocaleKeys.noLogFiles.tr(), - type: ToastificationType.error, - ); - } - return; - } - - final archive = Archive(); - for (final file in archiveLogFiles) { - archive.addFile(file); - } - - 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); - - 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, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/util/string_extension.dart b/frontend/appflowy_flutter/lib/util/string_extension.dart deleted file mode 100644 index 84dba35e1a..0000000000 --- a/frontend/appflowy_flutter/lib/util/string_extension.dart +++ /dev/null @@ -1,94 +0,0 @@ -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; - -extension StringExtension on String { - static const _specialCharacters = r'\/:*?"<>| '; - - /// Encode a string to a file name. - /// - /// Normalizes the string to remove special characters and replaces the "\/:*?"<>|" with underscores. - String toFileName() { - final buffer = StringBuffer(); - for (final character in characters) { - if (_specialCharacters.contains(character)) { - buffer.write('_'); - } else { - buffer.write(character); - } - } - return buffer.toString(); - } - - /// Returns the file size of the file at the given path. - /// - /// Returns null if the file does not exist. - int? get fileSize { - final file = File(this); - if (file.existsSync()) { - return file.lengthSync(); - } - return null; - } - - /// Returns true if the string is a appflowy cloud url. - bool get isAppFlowyCloudUrl => appflowyCloudUrlRegex.hasMatch(this); - - /// Returns the color of the string. - /// - /// ONLY used for the cover. - Color? coverColor(BuildContext context) { - // try to parse the color from the tint id, - // 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/theme_extension.dart b/frontend/appflowy_flutter/lib/util/theme_extension.dart deleted file mode 100644 index c7b56699d3..0000000000 --- a/frontend/appflowy_flutter/lib/util/theme_extension.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:flutter/material.dart'; - -extension IsLightMode on ThemeData { - bool get isLightMode => brightness == Brightness.light; -} diff --git a/frontend/appflowy_flutter/lib/util/theme_mode_extension.dart b/frontend/appflowy_flutter/lib/util/theme_mode_extension.dart deleted file mode 100644 index 228339915a..0000000000 --- a/frontend/appflowy_flutter/lib/util/theme_mode_extension.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -extension LabelTextPhrasing on ThemeMode { - String get labelText => switch (this) { - ThemeMode.light => LocaleKeys.settings_appearance_themeMode_light.tr(), - ThemeMode.dark => LocaleKeys.settings_appearance_themeMode_dark.tr(), - ThemeMode.system => - LocaleKeys.settings_appearance_themeMode_system.tr(), - }; -} diff --git a/frontend/appflowy_flutter/lib/util/throttle.dart b/frontend/appflowy_flutter/lib/util/throttle.dart deleted file mode 100644 index 0aaa9f2d3a..0000000000 --- a/frontend/appflowy_flutter/lib/util/throttle.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:async'; - -class Throttler { - Throttler({ - this.duration = const Duration(milliseconds: 1000), - }); - - final Duration duration; - Timer? _timer; - - void call(Function callback) { - if (_timer?.isActive ?? false) return; - - _timer = Timer(duration, () { - callback(); - }); - } - - void cancel() { - _timer?.cancel(); - } - - void dispose() { - _timer?.cancel(); - _timer = null; - } -} diff --git a/frontend/appflowy_flutter/lib/util/time.dart b/frontend/appflowy_flutter/lib/util/time.dart deleted file mode 100644 index cdeb9834fc..0000000000 --- a/frontend/appflowy_flutter/lib/util/time.dart +++ /dev/null @@ -1,43 +0,0 @@ -final RegExp timerRegExp = - RegExp(r'(?:(?\d*)h)? ?(?:(?\d*)m)?'); - -int? parseTime(String timerStr) { - int? res = int.tryParse(timerStr); - if (res != null) { - return res; - } - - final matches = timerRegExp.firstMatch(timerStr); - if (matches == null) { - return null; - } - final hours = int.tryParse(matches.namedGroup('hours') ?? ""); - final minutes = int.tryParse(matches.namedGroup('minutes') ?? ""); - if (hours == null && minutes == null) { - return null; - } - - final expected = - "${hours != null ? '${hours}h' : ''}${hours != null && minutes != null ? ' ' : ''}${minutes != null ? '${minutes}m' : ''}"; - if (timerStr != expected) { - return null; - } - - res = 0; - res += hours != null ? hours * 60 : res; - res += minutes ?? 0; - - return res; -} - -String formatTime(int minutes) { - if (minutes >= 60) { - if (minutes % 60 == 0) { - return "${minutes ~/ 60}h"; - } - return "${minutes ~/ 60}h ${minutes % 60}m"; - } else if (minutes >= 0) { - return "${minutes}m"; - } - return ""; -} 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/action_navigation_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/action_navigation/action_navigation_bloc.dart deleted file mode 100644 index 5bc64b6785..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/action_navigation/action_navigation_bloc.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'action_navigation_bloc.freezed.dart'; - -class ActionNavigationBloc - extends Bloc { - ActionNavigationBloc() : super(const ActionNavigationState.initial()) { - on((event, emit) async { - await event.when( - performAction: (action, nextActions) async { - NavigationAction currentAction = action; - if (currentAction.arguments?[ActionArgumentKeys.view] == null && - action.type == ActionType.openView) { - final result = await ViewBackendService.getView(action.objectId); - final view = result.toNullable(); - if (view != null) { - if (currentAction.arguments == null) { - currentAction = currentAction.copyWith(arguments: {}); - } - - currentAction.arguments?.addAll({ActionArgumentKeys.view: view}); - } - } - - emit(state.copyWith(action: currentAction, nextActions: nextActions)); - - if (nextActions.isNotEmpty) { - final newActions = [...nextActions]; - final next = newActions.removeAt(0); - - add( - ActionNavigationEvent.performAction( - action: next, - nextActions: newActions, - ), - ); - } else { - emit(state.setNoAction()); - } - }, - ); - }); - } -} - -@freezed -class ActionNavigationEvent with _$ActionNavigationEvent { - const factory ActionNavigationEvent.performAction({ - required NavigationAction action, - @Default([]) List nextActions, - }) = _PerformAction; -} - -class ActionNavigationState { - const ActionNavigationState.initial() - : action = null, - nextActions = const []; - - const ActionNavigationState({ - required this.action, - this.nextActions = const [], - }); - - final NavigationAction? action; - final List nextActions; - - ActionNavigationState copyWith({ - NavigationAction? action, - List? nextActions, - }) => - ActionNavigationState( - action: action ?? this.action, - nextActions: nextActions ?? this.nextActions, - ); - - ActionNavigationState setNoAction() => - const ActionNavigationState(action: null); -} 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 deleted file mode 100644 index 52663ea219..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart +++ /dev/null @@ -1,41 +0,0 @@ -enum ActionType { - openView, - jumpToBlock, - openRow, -} - -class ActionArgumentKeys { - static String view = "view"; - static String nodePath = "node_path"; - static String blockId = "block_id"; - static String rowId = "row_id"; -} - -/// A [NavigationAction] is used to communicate with the -/// [ActionNavigationBloc] to perform actions based on an event -/// triggered by pressing a notification, such as opening a specific -/// view and jumping to a specific block. -/// -class NavigationAction { - const NavigationAction({ - this.type = ActionType.openView, - this.arguments, - required this.objectId, - }); - - final ActionType type; - - final String objectId; - final Map? arguments; - - NavigationAction copyWith({ - ActionType? type, - String? objectId, - Map? arguments, - }) => - NavigationAction( - type: type ?? this.type, - objectId: objectId ?? this.objectId, - arguments: arguments ?? this.arguments, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart new file mode 100644 index 0000000000..84a61334c1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart @@ -0,0 +1,286 @@ +import 'dart:collection'; + +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:expandable/expandable.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; + +part 'app_bloc.freezed.dart'; + +class AppBloc extends Bloc { + final ViewBackendService appService; + final ViewListener viewListener; + + AppBloc({required ViewPB view}) + : appService = ViewBackendService(), + viewListener = ViewListener(viewId: view.id), + super(AppState.initial(view)) { + on((event, emit) async { + await event.map( + initial: (e) async { + _startListening(); + await _loadViews(emit); + }, + createView: (CreateView value) async { + await _createView(value, emit); + }, + loadViews: (_) async { + await _loadViews(emit); + }, + delete: (e) async { + await _deleteApp(emit); + }, + deleteView: (deletedView) async { + await _deleteView(emit, deletedView.viewId); + }, + rename: (e) async { + await _renameView(e, emit); + }, + appDidUpdate: (e) async { + final latestCreatedView = state.latestCreatedView; + final views = e.view.childViews; + AppState newState = state.copyWith( + views: views, + view: e.view, + ); + if (latestCreatedView != null) { + final index = views + .indexWhere((element) => element.id == latestCreatedView.id); + if (index == -1) { + newState = newState.copyWith(latestCreatedView: null); + } + emit(newState); + } + emit(newState); + }, + ); + }); + } + + void _startListening() { + viewListener.start( + onViewUpdated: (app) { + if (!isClosed) { + add(AppEvent.appDidUpdate(app)); + } + }, + ); + } + + Future _renameView(Rename e, Emitter emit) async { + final result = await ViewBackendService.updateView( + viewId: state.view.id, + name: e.newName, + ); + result.fold( + (l) => emit(state.copyWith(successOrFailure: left(unit))), + (error) => emit(state.copyWith(successOrFailure: right(error))), + ); + } + +// Delete the current app + Future _deleteApp(Emitter emit) async { + final result = await ViewBackendService.delete(viewId: state.view.id); + result.fold( + (unit) => emit(state.copyWith(successOrFailure: left(unit))), + (error) => emit(state.copyWith(successOrFailure: right(error))), + ); + } + + Future _deleteView(Emitter emit, String viewId) async { + final result = await ViewBackendService.deleteView(viewId: viewId); + result.fold( + (unit) => emit(state.copyWith(successOrFailure: left(unit))), + (error) => emit(state.copyWith(successOrFailure: right(error))), + ); + } + + Future _createView(CreateView value, Emitter emit) async { + // create a child view for the current view + final result = await ViewBackendService.createView( + parentViewId: state.view.id, + name: value.name, + desc: value.desc ?? "", + layoutType: value.layoutType, + initialDataBytes: value.initialDataBytes, + ext: value.ext ?? {}, + openAfterCreate: true, + ); + result.fold( + (view) => emit( + state.copyWith( + latestCreatedView: value.openAfterCreated ? view : null, + successOrFailure: left(unit), + ), + ), + (error) { + Log.error(error); + emit(state.copyWith(successOrFailure: right(error))); + }, + ); + } + + @override + Future close() async { + await viewListener.stop(); + return super.close(); + } + + Future _loadViews(Emitter emit) async { + final viewsOrFailed = + await ViewBackendService.getChildViews(viewId: state.view.id); + viewsOrFailed.fold( + (views) => emit(state.copyWith(views: views)), + (error) { + Log.error(error); + emit(state.copyWith(successOrFailure: right(error))); + }, + ); + } +} + +@freezed +class AppEvent with _$AppEvent { + const factory AppEvent.initial() = Initial; + const factory AppEvent.createView( + String name, + ViewLayoutPB layoutType, { + String? desc, + + /// ~~The initial data should be the JSON of the document~~ + /// ~~For example: {"document":{"type":"editor","children":[]}}~~ + /// + /// - Document: + /// the initial data should be the string that can be converted into [DocumentDataPB] + /// + List? initialDataBytes, + Map? ext, + + /// open the view after created + @Default(true) bool openAfterCreated, + }) = CreateView; + const factory AppEvent.loadViews() = LoadApp; + const factory AppEvent.delete() = DeleteApp; + const factory AppEvent.deleteView(String viewId) = DeleteView; + const factory AppEvent.rename(String newName) = Rename; + const factory AppEvent.appDidUpdate(ViewPB view) = AppDidUpdate; +} + +@freezed +class AppState with _$AppState { + const factory AppState({ + required ViewPB view, + required List views, + ViewPB? latestCreatedView, + required Either successOrFailure, + }) = _AppState; + + factory AppState.initial(ViewPB view) => AppState( + view: view, + views: view.childViews, + successOrFailure: left(unit), + ); +} + +class ViewDataContext extends ChangeNotifier { + final String viewId; + final ValueNotifier> _viewsNotifier = ValueNotifier([]); + final ValueNotifier _selectedViewNotifier = ValueNotifier(null); + VoidCallback? _menuSharedStateListener; + ExpandableController expandController = + ExpandableController(initialExpanded: false); + + ViewDataContext({required this.viewId}) { + _setLatestView(getIt().latestOpenView); + _menuSharedStateListener = + getIt().addLatestViewListener((view) { + _setLatestView(view); + }); + } + + VoidCallback onViewSelected(void Function(ViewPB?) callback) { + listener() { + callback(_selectedViewNotifier.value); + } + + _selectedViewNotifier.addListener(listener); + return listener; + } + + void removeOnViewSelectedListener(VoidCallback listener) { + _selectedViewNotifier.removeListener(listener); + } + + void _setLatestView(ViewPB? view) { + view?.freeze(); + + if (_selectedViewNotifier.value != view) { + _selectedViewNotifier.value = view; + _expandIfNeed(); + notifyListeners(); + } + } + + ViewPB? get selectedView => _selectedViewNotifier.value; + + set views(List views) { + if (_viewsNotifier.value != views) { + _viewsNotifier.value = views; + notifyListeners(); + } + } + + UnmodifiableListView get views => + UnmodifiableListView(_viewsNotifier.value); + + VoidCallback onViewsChanged( + void Function(UnmodifiableListView) callback, + ) { + listener() { + callback(views); + } + + _viewsNotifier.addListener(listener); + return listener; + } + + void removeOnViewChangedListener(VoidCallback listener) { + _viewsNotifier.removeListener(listener); + } + + void _expandIfNeed() { + if (_selectedViewNotifier.value == null) { + return; + } + + if (!_viewsNotifier.value + .map((e) => e.id) + .toList() + .contains(_selectedViewNotifier.value?.id)) { + return; + } + + if (expandController.expanded == false) { + // Workaround: Delay 150 milliseconds to make the smooth animation while expanding + Future.delayed(const Duration(milliseconds: 150), () { + expandController.expanded = true; + }); + } + } + + @override + void dispose() { + if (_menuSharedStateListener != null) { + getIt() + .removeLatestViewListener(_menuSharedStateListener!); + } + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart new file mode 100644 index 0000000000..9365c03e5f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart @@ -0,0 +1 @@ +export 'app_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart new file mode 100644 index 0000000000..6bae98aa25 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart @@ -0,0 +1,416 @@ +import 'dart:async'; + +import 'package:appflowy/user/application/user_settings_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; +import 'package:easy_localization/easy_localization.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'appearance.freezed.dart'; + +const _white = Color(0xFFFFFFFF); + +/// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy. +/// It includes the [AppTheme], [ThemeMode], [TextStyles] and [Locale]. +class AppearanceSettingsCubit extends Cubit { + final AppearanceSettingsPB _setting; + + AppearanceSettingsCubit(AppearanceSettingsPB setting) + : _setting = setting, + super( + AppearanceSettingsState.initial( + setting.theme, + setting.themeMode, + setting.font, + setting.monospaceFont, + setting.locale, + setting.isMenuCollapsed, + setting.menuOffset, + ), + ); + + /// Update selected theme in the user's settings and emit an updated state + /// with the AppTheme named [themeName]. + void setTheme(String themeName) { + _setting.theme = themeName; + _saveAppearanceSettings(); + emit(state.copyWith(appTheme: AppTheme.fromName(themeName))); + } + + /// Update the theme mode in the user's settings and emit an updated state. + void setThemeMode(ThemeMode themeMode) { + _setting.themeMode = _themeModeToPB(themeMode); + _saveAppearanceSettings(); + emit(state.copyWith(themeMode: themeMode)); + } + + /// Updates the current locale and notify the listeners the locale was + /// changed. Fallback to [en] locale if [newLocale] is not supported. + void setLocale(BuildContext context, Locale newLocale) { + if (!context.supportedLocales.contains(newLocale)) { + Log.warn("Unsupported locale: $newLocale, Fallback to locale: en"); + newLocale = const Locale('en'); + } + + context.setLocale(newLocale).catchError((e) { + Log.warn('Catch error in setLocale: $e}'); + }); + + if (state.locale != newLocale) { + _setting.locale.languageCode = newLocale.languageCode; + _setting.locale.countryCode = newLocale.countryCode ?? ""; + _saveAppearanceSettings(); + emit(state.copyWith(locale: newLocale)); + } + } + + // Saves the menus current visibility + void saveIsMenuCollapsed(bool collapsed) { + _setting.isMenuCollapsed = collapsed; + _saveAppearanceSettings(); + } + + // Saves the current resize offset of the menu + void saveMenuOffset(double offset) { + _setting.menuOffset = offset; + _saveAppearanceSettings(); + } + + /// Saves key/value setting to disk. + /// Removes the key if the passed in value is null + void setKeyValue(String key, String? value) { + if (key.isEmpty) { + Log.warn("The key should not be empty"); + return; + } + + if (value == null) { + _setting.settingKeyValue.remove(key); + } + + if (_setting.settingKeyValue[key] != value) { + if (value == null) { + _setting.settingKeyValue.remove(key); + } else { + _setting.settingKeyValue[key] = value; + } + } + _saveAppearanceSettings(); + } + + String? getValue(String key) { + if (key.isEmpty) { + Log.warn("The key should not be empty"); + return null; + } + return _setting.settingKeyValue[key]; + } + + /// Called when the application launches. + /// Uses the device locale when the application is opened for the first time. + void readLocaleWhenAppLaunch(BuildContext context) { + if (_setting.resetToDefault) { + _setting.resetToDefault = false; + _saveAppearanceSettings(); + setLocale(context, context.deviceLocale); + return; + } + + setLocale(context, state.locale); + } + + Future _saveAppearanceSettings() async { + UserSettingsBackendService().setAppearanceSetting(_setting).then((result) { + result.fold( + (l) => null, + (error) => Log.error(error), + ); + }); + } +} + +ThemeMode _themeModeFromPB(ThemeModePB themeModePB) { + switch (themeModePB) { + case ThemeModePB.Light: + return ThemeMode.light; + case ThemeModePB.Dark: + return ThemeMode.dark; + case ThemeModePB.System: + default: + return ThemeMode.system; + } +} + +ThemeModePB _themeModeToPB(ThemeMode themeMode) { + switch (themeMode) { + case ThemeMode.light: + return ThemeModePB.Light; + case ThemeMode.dark: + return ThemeModePB.Dark; + case ThemeMode.system: + default: + return ThemeModePB.System; + } +} + +@freezed +class AppearanceSettingsState with _$AppearanceSettingsState { + const AppearanceSettingsState._(); + + const factory AppearanceSettingsState({ + required AppTheme appTheme, + required ThemeMode themeMode, + required String font, + required String monospaceFont, + required Locale locale, + required bool isMenuCollapsed, + required double menuOffset, + }) = _AppearanceSettingsState; + + factory AppearanceSettingsState.initial( + String themeName, + ThemeModePB themeModePB, + String font, + String monospaceFont, + LocaleSettingsPB localePB, + bool isMenuCollapsed, + double menuOffset, + ) { + return AppearanceSettingsState( + appTheme: AppTheme.fromName(themeName), + font: font, + monospaceFont: monospaceFont, + themeMode: _themeModeFromPB(themeModePB), + locale: Locale(localePB.languageCode, localePB.countryCode), + isMenuCollapsed: isMenuCollapsed, + menuOffset: menuOffset, + ); + } + + ThemeData get lightTheme => _getThemeData(Brightness.light); + ThemeData get darkTheme => _getThemeData(Brightness.dark); + + ThemeData _getThemeData(Brightness brightness) { + // Poppins and SF Mono are not well supported in some languages, so use the + // built-in font for the following languages. + final useBuiltInFontLanguages = [ + const Locale('zh', 'CN'), + const Locale('zh', 'TW'), + ]; + String fontFamily = font; + String monospaceFontFamily = monospaceFont; + if (useBuiltInFontLanguages.contains(locale)) { + fontFamily = ''; + monospaceFontFamily = ''; + } + + final theme = brightness == Brightness.light + ? appTheme.lightTheme + : appTheme.darkTheme; + + final colorScheme = ColorScheme( + brightness: brightness, + primary: theme.primary, + onPrimary: theme.onPrimary, + primaryContainer: theme.main2, + onPrimaryContainer: _white, + // page title hover color + secondary: theme.hoverBG1, + onSecondary: theme.shader1, + // setting value hover color + secondaryContainer: theme.selector, + onSecondaryContainer: theme.topbarBg, + tertiary: theme.shader7, + // Editor: toolbarColor + onTertiary: theme.toolbarColor, + tertiaryContainer: theme.questionBubbleBG, + background: theme.surface, + onBackground: theme.text, + surface: theme.surface, + // text&icon color when it is hovered + onSurface: theme.hoverFG, + // grey hover color + inverseSurface: theme.hoverBG3, + onError: theme.shader7, + error: theme.red, + outline: theme.shader4, + surfaceVariant: theme.sidebarBg, + shadow: theme.shadow, + ); + + const Set scrollbarInteractiveStates = { + MaterialState.pressed, + MaterialState.hovered, + MaterialState.dragged, + }; + + return ThemeData( + brightness: brightness, + dialogBackgroundColor: theme.surface, + textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text), + textSelectionTheme: TextSelectionThemeData( + cursorColor: theme.main2, + selectionHandleColor: theme.main2, + ), + iconTheme: IconThemeData(color: theme.icon), + tooltipTheme: TooltipThemeData( + textStyle: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s11, + fontWeight: FontWeight.w400, + fontColor: theme.surface, + ), + ), + scaffoldBackgroundColor: theme.surface, + snackBarTheme: SnackBarThemeData( + backgroundColor: colorScheme.primary, + contentTextStyle: TextStyle(color: colorScheme.onSurface), + ), + scrollbarTheme: ScrollbarThemeData( + thumbColor: MaterialStateProperty.resolveWith((states) { + if (states.any(scrollbarInteractiveStates.contains)) { + return theme.shader7; + } + return theme.shader5; + }), + thickness: MaterialStateProperty.resolveWith((states) { + if (states.any(scrollbarInteractiveStates.contains)) { + return 4; + } + return 3.0; + }), + crossAxisMargin: 0.0, + mainAxisMargin: 6.0, + radius: Corners.s10Radius, + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + //dropdown menu color + canvasColor: theme.surface, + dividerColor: theme.divider, + hintColor: theme.hint, + //action item hover color + hoverColor: theme.hoverBG2, + disabledColor: theme.shader4, + highlightColor: theme.main1, + indicatorColor: theme.main1, + cardColor: theme.input, + colorScheme: colorScheme, + extensions: [ + AFThemeExtension( + warning: theme.yellow, + success: theme.green, + tint1: theme.tint1, + tint2: theme.tint2, + tint3: theme.tint3, + tint4: theme.tint4, + tint5: theme.tint5, + tint6: theme.tint6, + tint7: theme.tint7, + tint8: theme.tint8, + tint9: theme.tint9, + textColor: theme.text, + greyHover: theme.hoverBG1, + greySelect: theme.bg3, + lightGreyHover: theme.hoverBG3, + toggleOffFill: theme.shader5, + progressBarBGColor: theme.progressBarBGColor, + toggleButtonBGColor: theme.toggleButtonBGColor, + code: _getFontStyle( + fontFamily: monospaceFontFamily, + fontColor: theme.shader3, + ), + callout: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s11, + fontColor: theme.shader3, + ), + caption: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s11, + fontWeight: FontWeight.w400, + fontColor: theme.hint, + ), + ) + ], + ); + } + + TextStyle _getFontStyle({ + String? fontFamily, + double? fontSize, + FontWeight? fontWeight, + Color? fontColor, + double? letterSpacing, + double? lineHeight, + }) => + TextStyle( + fontFamily: fontFamily, + fontSize: fontSize ?? FontSizes.s12, + color: fontColor, + fontWeight: fontWeight ?? FontWeight.w500, + fontFamilyFallback: const ["Noto Color Emoji"], + letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005), + height: lineHeight, + ); + + TextTheme _getTextTheme({ + required String fontFamily, + required Color fontColor, + }) { + return TextTheme( + displayLarge: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s32, + fontColor: fontColor, + fontWeight: FontWeight.w600, + lineHeight: 42.0, + ), // h2 + displayMedium: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s24, + fontColor: fontColor, + fontWeight: FontWeight.w600, + lineHeight: 34.0, + ), // h3 + displaySmall: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s20, + fontColor: fontColor, + fontWeight: FontWeight.w600, + lineHeight: 28.0, + ), // h4 + titleLarge: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s18, + fontColor: fontColor, + fontWeight: FontWeight.w600, + ), // title + titleMedium: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s16, + fontColor: fontColor, + fontWeight: FontWeight.w600, + ), // heading + titleSmall: _getFontStyle( + fontFamily: fontFamily, + fontSize: FontSizes.s14, + fontColor: fontColor, + fontWeight: FontWeight.w600, + ), // subheading + bodyMedium: _getFontStyle( + fontFamily: fontFamily, + fontColor: fontColor, + ), // body-regular + bodySmall: _getFontStyle( + fontFamily: fontFamily, + fontColor: fontColor, + fontWeight: FontWeight.w400, + ), // body-thin + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart deleted file mode 100644 index c3190a8e40..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart +++ /dev/null @@ -1,19 +0,0 @@ -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 { - static const kDefaultFontFamily = defaultFontFamily; - static const kDefaultThemeMode = ThemeMode.system; - static const kDefaultThemeName = "Default"; - static const kDefaultTheme = BuiltInTheme.defaultTheme; - - static Color getDefaultCursorColor(BuildContext context) { - return Theme.of(context).colorScheme.primary; - } - - static Color getDefaultSelectionColor(BuildContext context) { - return Theme.of(context).colorScheme.primary.withValues(alpha: 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 deleted file mode 100644 index 01f638fe7a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ /dev/null @@ -1,348 +0,0 @@ -import 'dart:async'; - -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_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(); - } -} - -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); - - _initTrash(); - } - - final Debouncer _searchDebouncer = Debouncer( - delay: const Duration(milliseconds: 300), - ); - final TrashService _trashService = TrashService(); - final TrashListener _trashListener = TrashListener(); - String? _activeQuery; - String? _workspaceId; - - @override - Future close() { - _trashListener.close(); - _searchDebouncer.dispose(); - state.searchResponseStream?.dispose(); - return super.close(); - } - - Future _initTrash() async { - _trashListener.start( - trashUpdated: (trashOrFailed) => add( - CommandPaletteEvent.trashChanged( - trash: trashOrFailed.toNullable(), - ), - ), - ); - - final trashOrFailure = await _trashService.readTrash(); - trashOrFailure.fold( - (trash) => add(CommandPaletteEvent.trashChanged(trash: trash.items)), - (error) => debugPrint('Failed to load trash: $error'), - ); - } - - FutureOr _onSearchChanged( - _SearchChanged event, - Emitter emit, - ) { - _searchDebouncer.run( - () { - if (!isClosed) { - add(CommandPaletteEvent.performSearch(search: event.search)); - } - }, - ); - } - - 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; - - 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, - ), - ); - } - }, - ), - ), - ); - } - } - - 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, - ); - } - - 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, - ), - ); - } - - 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. - }); - } - } - - 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; -} - -@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, - }) = _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, - @Default([]) List trash, - @Default(null) String? searchId, - }) = _CommandPaletteState; - - factory CommandPaletteState.initial() => const CommandPaletteState( - searching: false, - generatingAIOverview: false, - ); -} 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 deleted file mode 100644 index 6b6ea6d5c0..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart +++ /dev/null @@ -1,54 +0,0 @@ -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; - } -} - -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 _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, - }; -} - -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 deleted file mode 100644 index 89e5b604f8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart +++ /dev/null @@ -1,131 +0,0 @@ -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( - String keyword, { - String? workspaceId, - }) 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), - ); - - 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; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart new file mode 100644 index 0000000000..9f74162120 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart @@ -0,0 +1,54 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:appflowy/core/notification/document_notification.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/rust_stream.dart'; + +class DocumentListener { + DocumentListener({ + required this.id, + }); + + final String id; + + StreamSubscription? _subscription; + DocumentNotificationParser? _parser; + + Function(DocEventPB docEvent)? didReceiveUpdate; + + void start({ + Function(DocEventPB docEvent)? didReceiveUpdate, + }) { + this.didReceiveUpdate = didReceiveUpdate; + + _parser = DocumentNotificationParser( + id: id, + callback: _callback, + ); + _subscription = RustStreamReceiver.listen( + (observable) => _parser?.parse(observable), + ); + } + + void _callback( + DocumentNotification ty, + Either result, + ) { + switch (ty) { + case DocumentNotification.DidReceiveUpdate: + result + .swap() + .map((r) => didReceiveUpdate?.call(DocEventPB.fromBuffer(r))); + break; + default: + break; + } + } + + Future stop() async { + await _subscription?.cancel(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_context.dart b/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_context.dart index b0b4406ac3..95fabf8396 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_context.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_context.dart @@ -2,15 +2,14 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; abstract class EditPanelContext extends Equatable { - const EditPanelContext({ - required this.identifier, - required this.title, - required this.child, - }); - final String identifier; final String title; final Widget child; + const EditPanelContext({ + required this.child, + required this.identifier, + required this.title, + }); @override List get props => [identifier]; diff --git a/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_panel_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_panel_bloc.dart index 52d36033fe..4935530732 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_panel_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_panel_bloc.dart @@ -1,6 +1,7 @@ import 'package:appflowy/workspace/application/edit_panel/edit_context.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; part 'edit_panel_bloc.freezed.dart'; @@ -9,10 +10,10 @@ class EditPanelBloc extends Bloc { on((event, emit) async { await event.map( startEdit: (e) async { - emit(state.copyWith(isEditing: true, editContext: e.context)); + emit(state.copyWith(isEditing: true, editContext: some(e.context))); }, endEdit: (value) async { - emit(state.copyWith(isEditing: false, editContext: null)); + emit(state.copyWith(isEditing: false, editContext: none())); }, ); }); @@ -30,11 +31,11 @@ class EditPanelEvent with _$EditPanelEvent { class EditPanelState with _$EditPanelState { const factory EditPanelState({ required bool isEditing, - required EditPanelContext? editContext, + required Option editContext, }) = _EditPanelState; - factory EditPanelState.initial() => const EditPanelState( + factory EditPanelState.initial() => EditPanelState( isEditing: false, - editContext: null, + editContext: none(), ); } 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..39ffa4f10a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart @@ -3,18 +3,19 @@ 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/code_block_node_parser.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.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-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:dartz/dartz.dart'; import 'package:easy_localization/easy_localization.dart'; enum DocumentExportType { json, markdown, text, - html, } class DocumentExporter { @@ -24,44 +25,34 @@ class DocumentExporter { final ViewPB view; - Future> export( - DocumentExportType type, { - String? path, - }) async { + Future> export(DocumentExportType type) async { final documentService = DocumentService(); - final result = await documentService.openDocument(documentId: view.id); - return result.fold( - (r) async { - final document = r.toDocument(); - if (document == null) { - return FlowyResult.failure( - FlowyError( - msg: LocaleKeys.settings_files_exportFileFail.tr(), - ), + final result = await documentService.openDocument(view: view); + return result.fold((error) => left(error), (r) { + final document = r.toDocument(); + if (document == null) { + return left( + FlowyError( + msg: LocaleKeys.settings_files_exportFileFail.tr(), + ), + ); + } + switch (type) { + case DocumentExportType.json: + return right(jsonEncode(document)); + case DocumentExportType.markdown: + final markdown = documentToMarkdown( + document, + customParsers: [ + const DividerNodeParser(), + const MathEquationNodeParser(), + const CodeBlockNodeParser(), + ], ); - } - switch (type) { - 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), - ); - } - case DocumentExportType.text: - throw UnimplementedError(); - case DocumentExportType.html: - final html = documentToHTML( - document, - ); - return FlowyResult.success(html); - } - }, - (error) => FlowyResult.failure(error), - ); + return right(markdown); + case DocumentExportType.text: + throw UnimplementedError(); + } + }); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart deleted file mode 100644 index 546b9ba13d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; -import 'package:appflowy/workspace/application/view/view_ext.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:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'favorite_listener.dart'; - -part 'favorite_bloc.freezed.dart'; - -class FavoriteBloc extends Bloc { - FavoriteBloc() : super(FavoriteState.initial()) { - _dispatch(); - } - - final _service = FavoriteService(); - final _listener = FavoriteListener(); - bool isReordering = false; - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - _listener.start( - favoritesUpdated: _onFavoritesUpdated, - ); - add(const FavoriteEvent.fetchFavorites()); - }, - fetchFavorites: () async { - final result = await _service.readFavorites(); - emit( - result.fold( - (favoriteViews) { - final views = favoriteViews.items.toList(); - final pinnedViews = - views.where((v) => v.item.isPinned).toList(); - final unpinnedViews = - views.where((v) => !v.item.isPinned).toList(); - return state.copyWith( - isLoading: false, - views: views, - pinnedViews: pinnedViews, - unpinnedViews: unpinnedViews, - ); - }, - (error) => state.copyWith( - isLoading: false, - views: [], - ), - ), - ); - }, - toggle: (view) async { - final isFavorited = state.views.any((v) => v.item.id == view.id); - if (isFavorited) { - await _service.unpinFavorite(view); - } else if (state.pinnedViews.length < 3) { - // pin the view if there are less than 3 pinned views - await _service.pinFavorite(view); - } - - await _service.toggleFavorite(view.id); - }, - pin: (view) async { - await _service.pinFavorite(view); - add(const FavoriteEvent.fetchFavorites()); - }, - unpin: (view) async { - 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; - }, - ); - }, - ); - } - - void _onFavoritesUpdated( - FlowyResult favoriteOrFailed, - bool didFavorite, - ) { - if (!isReordering) { - 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 -class FavoriteState with _$FavoriteState { - const factory FavoriteState({ - @Default([]) List views, - @Default([]) List pinnedViews, - @Default([]) List unpinnedViews, - @Default(true) bool isLoading, - }) = _FavoriteState; - - factory FavoriteState.initial() => const FavoriteState(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart deleted file mode 100644 index 0cadf9c91f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/core/notification/folder_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.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'; -import 'package:flutter/foundation.dart'; - -typedef FavoriteUpdated = void Function( - FlowyResult result, - bool isFavorite, -); - -class FavoriteListener { - StreamSubscription? _streamSubscription; - FolderNotificationParser? _parser; - - FavoriteUpdated? _favoriteUpdated; - - void start({ - FavoriteUpdated? favoritesUpdated, - }) { - _favoriteUpdated = favoritesUpdated; - _parser = FolderNotificationParser( - id: 'favorite', - callback: _observableCallback, - ); - _streamSubscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - void _observableCallback( - FolderNotification ty, - FlowyResult result, - ) { - switch (ty) { - case FolderNotification.DidFavoriteView: - result.onSuccess( - (success) => _favoriteUpdated?.call( - FlowyResult.success(RepeatedViewPB.fromBuffer(success)), - true, - ), - ); - case FolderNotification.DidUnfavoriteView: - result.map( - (success) => _favoriteUpdated?.call( - FlowyResult.success(RepeatedViewPB.fromBuffer(success)), - false, - ), - ); - break; - default: - break; - } - } - - Future stop() async { - _parser = null; - await _streamSubscription?.cancel(); - _streamSubscription = null; - _favoriteUpdated = null; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart deleted file mode 100644 index 7f0f844dda..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.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_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; - -class FavoriteService { - Future> readFavorites() { - final result = FolderEventReadFavorites().send(); - return result.then((result) { - return result.fold( - (favoriteViews) { - return FlowyResult.success( - RepeatedFavoriteViewPB( - items: favoriteViews.items.where((e) => !e.item.isSpace), - ), - ); - }, - (error) => FlowyResult.failure(error), - ); - }); - } - - Future> toggleFavorite(String viewId) async { - final id = RepeatedViewIdPB.create()..items.add(viewId); - return FolderEventToggleFavorite(id).send(); - } - - Future> pinFavorite(ViewPB view) async { - return pinOrUnpinFavorite(view, true); - } - - Future> unpinFavorite(ViewPB view) async { - return pinOrUnpinFavorite(view, false); - } - - Future> pinOrUnpinFavorite( - ViewPB view, - bool isPinned, - ) async { - try { - final current = - view.extra.isNotEmpty ? jsonDecode(view.extra) : {}; - final merged = mergeMaps( - current, - {ViewExtKeys.isPinnedKey: isPinned}, - ); - await ViewBackendService.updateView( - viewId: view.id, - extra: jsonEncode(merged), - ); - } catch (e) { - return FlowyResult.failure(FlowyError(msg: 'Failed to pin favorite: $e')); - } - - return FlowyResult.success(null); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/prelude.dart deleted file mode 100644 index 15a3302d43..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/prelude.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'favorite_bloc.dart'; -export 'favorite_listener.dart'; -export 'favorite_service.dart'; 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..219910403b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -1,30 +1,25 @@ import 'package:appflowy/user/application/user_listener.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:flowy_infra/time/duration.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; +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-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart' + show WorkspaceSettingPB; +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'; - +import 'package:dartz/dartz.dart'; part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { - HomeBloc(WorkspaceLatestPB workspaceSetting) - : _workspaceListener = FolderListener(), + final UserWorkspaceListener _listener; + + HomeBloc( + UserProfilePB user, + WorkspaceSettingPB workspaceSetting, + ) : _listener = UserWorkspaceListener(userProfile: user), super(HomeState.initial(workspaceSetting)) { - _dispatch(workspaceSetting); - } - - final FolderListener _workspaceListener; - - @override - Future close() async { - await _workspaceListener.stop(); - return super.close(); - } - - void _dispatch(WorkspaceLatestPB workspaceSetting) { on( (event, emit) async { await event.map( @@ -35,10 +30,12 @@ class HomeBloc extends Bloc { } }); - _workspaceListener.start( - onLatestUpdated: (result) { + _listener.start( + onAuthChanged: (result) => _authDidChanged(result), + onSettingUpdated: (result) { result.fold( - (latest) => add(HomeEvent.didReceiveWorkspaceSetting(latest)), + (setting) => + add(HomeEvent.didReceiveWorkspaceSetting(setting)), (r) => Log.error(r), ); }, @@ -48,17 +45,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 + final latestView = workspaceSetting.hasLatestView() + ? workspaceSetting.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, @@ -66,10 +56,43 @@ class HomeBloc extends Bloc { ), ); }, + unauthorized: (_Unauthorized value) { + emit(state.copyWith(unauthorized: true)); + }, ); }, ); } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } + + void _authDidChanged(Either errorOrNothing) { + errorOrNothing.fold((_) {}, (error) { + if (error.code == ErrorCode.UserUnauthorized.value) { + add(HomeEvent.unauthorized(error.msg)); + } + }); + } +} + +enum MenuResizeType { + slide, + drag, +} + +extension MenuResizeTypeExtension on MenuResizeType { + Duration duration() { + switch (this) { + case MenuResizeType.drag: + return 30.milliseconds; + case MenuResizeType.slide: + return 350.milliseconds; + } + } } @freezed @@ -77,20 +100,24 @@ 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; + const factory HomeEvent.unauthorized(String msg) = _Unauthorized; } @freezed class HomeState with _$HomeState { const factory HomeState({ required bool isLoading, - required WorkspaceLatestPB workspaceSetting, + required WorkspaceSettingPB workspaceSetting, ViewPB? latestView, + required bool unauthorized, }) = _HomeState; - factory HomeState.initial(WorkspaceLatestPB workspaceSetting) => HomeState( + factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState( isLoading: false, workspaceSetting: workspaceSetting, + latestView: null, + unauthorized: false, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_service.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_service.dart new file mode 100644 index 0000000000..dd1c19b2b7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_service.dart @@ -0,0 +1,14 @@ +import 'dart:async'; + +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; + +class HomeService { + Future> readApp({required String appId}) { + final payload = ViewIdPB.create()..value = appId; + + return FolderEventReadView(payload).send(); + } +} 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..f0c0566137 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 @@ -1,9 +1,10 @@ import 'package:appflowy/user/application/user_listener.dart'; +import 'package:appflowy/workspace/application/appearance.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; -import 'package:flowy_infra/size.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart' + show WorkspaceSettingPB; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:dartz/dartz.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -11,41 +12,30 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'home_setting_bloc.freezed.dart'; class HomeSettingBloc extends Bloc { + final UserWorkspaceListener _listener; + final AppearanceSettingsCubit _appearanceSettingsCubit; + HomeSettingBloc( - WorkspaceLatestPB workspaceSetting, + UserProfilePB user, + WorkspaceSettingPB workspaceSetting, AppearanceSettingsCubit appearanceSettingsCubit, - double screenWidthPx, - ) : _listener = FolderListener(), + ) : _listener = UserWorkspaceListener(userProfile: user), _appearanceSettingsCubit = appearanceSettingsCubit, super( HomeSettingState.initial( workspaceSetting, appearanceSettingsCubit.state, - screenWidthPx, ), ) { - _dispatch(); - } - - final FolderListener _listener; - final AppearanceSettingsCubit _appearanceSettingsCubit; - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } - - void _dispatch() { on( (event, emit) async { await event.map( initial: (_Initial value) {}, setEditPanel: (e) async { - emit(state.copyWith(panelContext: e.editContext)); + emit(state.copyWith(panelContext: some(e.editContext))); }, dismissEditPanel: (value) async { - emit(state.copyWith(panelContext: null)); + emit(state.copyWith(panelContext: none())); }, didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { emit(state.copyWith(workspaceSetting: value.setting)); @@ -53,28 +43,7 @@ class HomeSettingBloc extends Bloc { collapseMenu: (_CollapseMenu e) { final isMenuCollapsed = !state.isMenuCollapsed; _appearanceSettingsCubit.saveIsMenuCollapsed(isMenuCollapsed); - emit( - state.copyWith( - isMenuCollapsed: isMenuCollapsed, - keepMenuCollapsed: isMenuCollapsed, - ), - ); - }, - checkScreenSize: (_CheckScreenSize e) { - final bool isScreenSmall = - e.screenWidthPx < PageBreaks.tabletLandscape; - - if (state.isScreenSmall != isScreenSmall) { - final isMenuCollapsed = isScreenSmall || state.keepMenuCollapsed; - emit( - state.copyWith( - isMenuCollapsed: isMenuCollapsed, - isScreenSmall: isScreenSmall, - ), - ); - } else { - emit(state.copyWith(isScreenSmall: isScreenSmall)); - } + emit(state.copyWith(isMenuCollapsed: isMenuCollapsed)); }, editPanelResizeStart: (_EditPanelResizeStart e) { emit( @@ -86,7 +55,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)); } @@ -99,6 +68,12 @@ class HomeSettingBloc extends Bloc { }, ); } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } } enum MenuResizeType { @@ -124,11 +99,9 @@ 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) = - _CheckScreenSize; const factory HomeSettingEvent.editPanelResized(double offset) = _EditPanelResized; const factory HomeSettingEvent.editPanelResizeStart() = _EditPanelResizeStart; @@ -138,32 +111,26 @@ class HomeSettingEvent with _$HomeSettingEvent { @freezed class HomeSettingState with _$HomeSettingState { const factory HomeSettingState({ - required EditPanelContext? panelContext, - required WorkspaceLatestPB workspaceSetting, + required Option panelContext, + required WorkspaceSettingPB workspaceSetting, required bool unauthorized, required bool isMenuCollapsed, - required bool keepMenuCollapsed, - required bool isScreenSmall, required double resizeOffset, required double resizeStart, required MenuResizeType resizeType, }) = _HomeSettingState; factory HomeSettingState.initial( - WorkspaceLatestPB workspaceSetting, + WorkspaceSettingPB workspaceSetting, AppearanceSettingsState appearanceSettingsState, - double screenWidthPx, - ) { - return HomeSettingState( - panelContext: null, - workspaceSetting: workspaceSetting, - unauthorized: false, - isMenuCollapsed: appearanceSettingsState.isMenuCollapsed, - isScreenSmall: screenWidthPx < PageBreaks.tabletLandscape, - keepMenuCollapsed: false, - resizeOffset: appearanceSettingsState.menuOffset, - resizeStart: 0, - resizeType: MenuResizeType.slide, - ); - } + ) => + HomeSettingState( + panelContext: none(), + workspaceSetting: workspaceSetting, + unauthorized: false, + isMenuCollapsed: appearanceSettingsState.isMenuCollapsed, + resizeOffset: appearanceSettingsState.menuOffset, + resizeStart: 0, + resizeType: MenuResizeType.slide, + ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart new file mode 100644 index 0000000000..aa876fae61 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart @@ -0,0 +1,132 @@ +import 'dart:async'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'menu_bloc.freezed.dart'; + +class MenuBloc extends Bloc { + final WorkspaceService _workspaceService; + final WorkspaceListener _listener; + final UserProfilePB user; + final WorkspacePB workspace; + + MenuBloc({ + required this.user, + required this.workspace, + }) : _workspaceService = WorkspaceService(workspaceId: workspace.id), + _listener = WorkspaceListener( + user: user, + workspaceId: workspace.id, + ), + super(MenuState.initial(workspace)) { + on((event, emit) async { + await event.map( + initial: (e) async { + _listener.start(appsChanged: _handleAppsOrFail); + await _fetchApps(emit); + }, + openPage: (e) async { + emit(state.copyWith(plugin: e.plugin)); + }, + createApp: (_CreateApp event) async { + final result = await _workspaceService.createApp( + name: event.name, + desc: event.desc ?? "", + ); + result.fold( + (app) => {}, + (error) { + Log.error(error); + emit(state.copyWith(successOrFailure: right(error))); + }, + ); + }, + didReceiveApps: (e) async { + emit( + e.appsOrFail.fold( + (views) => + state.copyWith(views: views, successOrFailure: left(unit)), + (err) => state.copyWith(successOrFailure: right(err)), + ), + ); + }, + moveApp: (_MoveApp value) { + if (state.views.length > value.fromIndex) { + final view = state.views[value.fromIndex]; + _workspaceService.moveApp( + appId: view.id, + fromIndex: value.fromIndex, + toIndex: value.toIndex, + ); + final apps = List.from(state.views); + + apps.insert(value.toIndex, apps.removeAt(value.fromIndex)); + emit(state.copyWith(views: apps)); + } + }, + ); + }); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } + + // ignore: unused_element + Future _fetchApps(Emitter emit) async { + final appsOrFail = await _workspaceService.getViews(); + emit( + appsOrFail.fold( + (views) => state.copyWith(views: views), + (error) { + Log.error(error); + return state.copyWith(successOrFailure: right(error)); + }, + ), + ); + } + + void _handleAppsOrFail(Either, FlowyError> appsOrFail) { + appsOrFail.fold( + (apps) => add(MenuEvent.didReceiveApps(left(apps))), + (error) => add(MenuEvent.didReceiveApps(right(error))), + ); + } +} + +@freezed +class MenuEvent with _$MenuEvent { + const factory MenuEvent.initial() = _Initial; + const factory MenuEvent.openPage(Plugin plugin) = _OpenPage; + const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp; + const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp; + const factory MenuEvent.didReceiveApps( + Either, FlowyError> appsOrFail, + ) = _ReceiveApps; +} + +@freezed +class MenuState with _$MenuState { + const factory MenuState({ + required List views, + required Either successOrFailure, + required Plugin plugin, + }) = _MenuState; + + factory MenuState.initial(WorkspacePB workspace) => MenuState( + views: workspace.views, + successOrFailure: left(unit), + plugin: makePlugin(pluginType: PluginType.blank), + ); +} 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..88ad170e91 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 @@ -1,58 +1,59 @@ import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.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/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'; +import 'package:dartz/dartz.dart'; part 'menu_user_bloc.freezed.dart'; class MenuUserBloc extends Bloc { - MenuUserBloc(this.userProfile) - : _userListener = UserListener(userProfile: userProfile), - _userWorkspaceListener = FolderListener(), - _userService = UserBackendService(userId: userProfile.id), - super(MenuUserState.initial(userProfile)) { - _dispatch(); - } - final UserBackendService _userService; final UserListener _userListener; - final FolderListener _userWorkspaceListener; + final UserWorkspaceListener _userWorkspaceListener; final UserProfilePB userProfile; + MenuUserBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + _userWorkspaceListener = + UserWorkspaceListener(userProfile: userProfile), + _userService = UserBackendService(userId: userProfile.id), + super(MenuUserState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + _userWorkspaceListener.start( + onWorkspacesUpdated: _workspaceListUpdated, + ); + await _initUser(); + }, + fetchWorkspaces: () async { + // + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + updateUserName: (String name) { + _userService.updateUserProfile(name: name).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + ); + }); + } + @override Future close() async { await _userListener.stop(); await _userWorkspaceListener.stop(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - _userListener.start(onProfileUpdated: _profileUpdated); - await _initUser(); - }, - didReceiveUserProfile: (UserProfilePB newUserProfile) { - emit(state.copyWith(userProfile: newUserProfile)); - }, - updateUserName: (String name) { - _userService.updateUserProfile(name: name).then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - ); - }, - ); + super.close(); } Future _initUser() async { @@ -60,22 +61,25 @@ class MenuUserBloc extends Bloc { result.fold((l) => null, (error) => Log.error(error)); } - void _profileUpdated( - FlowyResult userProfileOrFailed, - ) { - if (isClosed) { - return; - } + void _profileUpdated(Either userProfileOrFailed) { userProfileOrFailed.fold( - (profile) => add(MenuUserEvent.didReceiveUserProfile(profile)), + (newUserProfile) => + add(MenuUserEvent.didReceiveUserProfile(newUserProfile)), (err) => Log.error(err), ); } + + void _workspaceListUpdated( + Either, FlowyError> workspacesOrFailed, + ) { + // Do nothing by now + } } @freezed class MenuUserEvent with _$MenuUserEvent { const factory MenuUserEvent.initial() = _Initial; + const factory MenuUserEvent.fetchWorkspaces() = _FetchWorkspaces; const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName; const factory MenuUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, @@ -86,13 +90,13 @@ class MenuUserEvent with _$MenuUserEvent { class MenuUserState with _$MenuUserState { const factory MenuUserState({ required UserProfilePB userProfile, - required List? workspaces, - required FlowyResult successOrFailure, + required Option> workspaces, + required Either successOrFailure, }) = _MenuUserState; factory MenuUserState.initial(UserProfilePB userProfile) => MenuUserState( userProfile: userProfile, - workspaces: null, - successOrFailure: FlowyResult.success(null), + workspaces: none(), + successOrFailure: left(unit), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart new file mode 100644 index 0000000000..a5b7130dc8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/app/app_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'menu_view_section_bloc.freezed.dart'; + +class ViewSectionBloc extends Bloc { + void Function()? _viewsListener; + void Function()? _selectedViewlistener; + final ViewDataContext _appViewData; + + ViewSectionBloc({ + required ViewDataContext appViewData, + }) : _appViewData = appViewData, + super(ViewSectionState.initial(appViewData)) { + on((event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + setSelectedView: (view) { + emit(state.copyWith(selectedView: view)); + }, + didReceiveViewUpdated: (views) { + emit(state.copyWith(views: views)); + }, + moveView: (fromIndex, toIndex) async { + _moveView(fromIndex, toIndex, emit); + }, + ); + }); + } + + void _startListening() { + _viewsListener = _appViewData.onViewsChanged((views) { + if (!isClosed) { + add(ViewSectionEvent.didReceiveViewUpdated(views)); + } + }); + _selectedViewlistener = _appViewData.onViewSelected((view) { + if (!isClosed) { + add(ViewSectionEvent.setSelectedView(view)); + } + }); + } + + Future _moveView( + int fromIndex, + int toIndex, + Emitter emit, + ) async { + if (fromIndex < state.views.length) { + final viewId = state.views[fromIndex].id; + final views = List.from(state.views); + views.insert(toIndex, views.removeAt(fromIndex)); + emit(state.copyWith(views: views)); + + final result = await ViewBackendService.moveView( + viewId: viewId, + fromIndex: fromIndex, + toIndex: toIndex, + ); + result.fold((l) => null, (err) => Log.error(err)); + } + } + + @override + Future close() async { + if (_selectedViewlistener != null) { + _appViewData.removeOnViewSelectedListener(_selectedViewlistener!); + } + + if (_viewsListener != null) { + _appViewData.removeOnViewChangedListener(_viewsListener!); + } + + return super.close(); + } +} + +@freezed +class ViewSectionEvent with _$ViewSectionEvent { + const factory ViewSectionEvent.initial() = _Initial; + const factory ViewSectionEvent.setSelectedView(ViewPB? view) = + _SetSelectedView; + const factory ViewSectionEvent.moveView(int fromIndex, int toIndex) = + _MoveView; + const factory ViewSectionEvent.didReceiveViewUpdated(List views) = + _DidReceiveViewUpdated; +} + +@freezed +class ViewSectionState with _$ViewSectionState { + const factory ViewSectionState({ + required List views, + ViewPB? selectedView, + }) = _ViewSectionState; + + factory ViewSectionState.initial(ViewDataContext appViewData) => + ViewSectionState( + views: appViewData.views, + selectedView: appViewData.selectedView, + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart index 0412a9956d..0bf94ea60b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart @@ -1,2 +1,2 @@ +export 'menu_bloc.dart'; export 'menu_user_bloc.dart'; -export 'sidebar_sections_bloc.dart'; 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 deleted file mode 100644 index d6a6a73578..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart +++ /dev/null @@ -1,317 +0,0 @@ -import 'dart:async'; - -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/workspace/workspace_sections_listener.dart'; -import 'package:appflowy/workspace/application/workspace/workspace_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/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'; - -part 'sidebar_sections_bloc.freezed.dart'; - -class SidebarSection { - const SidebarSection({ - required this.publicViews, - required this.privateViews, - }); - - const SidebarSection.empty() - : publicViews = const [], - privateViews = const []; - - final List publicViews; - final List privateViews; - - List get views => publicViews + privateViews; - - SidebarSection copyWith({ - List? publicViews, - List? privateViews, - }) { - return SidebarSection( - publicViews: publicViews ?? this.publicViews, - privateViews: privateViews ?? this.privateViews, - ); - } -} - -/// The [SidebarSectionsBloc] is responsible for -/// managing the root views in different sections of the workspace. -class SidebarSectionsBloc - extends Bloc { - SidebarSectionsBloc() : super(SidebarSectionsState.initial()) { - on( - (event, emit) async { - await event.when( - initial: (userProfile, workspaceId) async { - _initial(userProfile, workspaceId); - final sectionViews = await _getSectionViews(); - if (sectionViews != null) { - final containsSpace = _containsSpace(sectionViews); - emit( - state.copyWith( - section: sectionViews, - containsSpace: containsSpace, - ), - ); - } - }, - reset: (userProfile, workspaceId) async { - _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 { - final result = await _workspaceService.createView( - name: name, - viewSection: section, - index: index, - ); - result.fold( - (view) => emit( - state.copyWith( - lastCreatedRootView: view, - createRootViewResult: FlowyResult.success(null), - ), - ), - (error) { - Log.error('Failed to create root view: $error'); - emit( - state.copyWith( - createRootViewResult: FlowyResult.failure(error), - ), - ); - }, - ); - }, - receiveSectionViewsUpdate: (sectionViews) async { - final section = sectionViews.section; - switch (section) { - case ViewSectionPB.Public: - emit( - state.copyWith( - containsSpace: state.containsSpace || - sectionViews.views.any((view) => view.isSpace), - section: state.section.copyWith( - publicViews: sectionViews.views, - ), - ), - ); - case ViewSectionPB.Private: - emit( - state.copyWith( - containsSpace: state.containsSpace || - sectionViews.views.any((view) => view.isSpace), - section: state.section.copyWith( - privateViews: sectionViews.views, - ), - ), - ); - break; - default: - break; - } - }, - moveRootView: (fromIndex, toIndex, fromSection, toSection) async { - final views = fromSection == ViewSectionPB.Public - ? List.from(state.section.publicViews) - : List.from(state.section.privateViews); - if (fromIndex < 0 || fromIndex >= views.length) { - Log.error( - 'Invalid fromIndex: $fromIndex, maxIndex: ${views.length - 1}', - ); - return; - } - final view = views[fromIndex]; - final result = await _workspaceService.moveView( - viewId: view.id, - fromIndex: fromIndex, - toIndex: toIndex, - ); - result.fold( - (value) { - views.insert(toIndex, views.removeAt(fromIndex)); - var newState = state; - if (fromSection == ViewSectionPB.Public) { - newState = newState.copyWith( - section: newState.section.copyWith(publicViews: views), - ); - } else if (fromSection == ViewSectionPB.Private) { - newState = newState.copyWith( - section: newState.section.copyWith(privateViews: views), - ); - } - emit(newState); - }, - (error) { - Log.error('Failed to move root view: $error'); - }, - ); - }, - reload: (userProfile, workspaceId) async { - _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 - if (sectionViews.publicViews.isNotEmpty) { - getIt().add( - TabsEvent.openPlugin( - plugin: sectionViews.publicViews.first.plugin(), - ), - ); - } else if (sectionViews.privateViews.isNotEmpty) { - getIt().add( - TabsEvent.openPlugin( - plugin: sectionViews.privateViews.first.plugin(), - ), - ); - } else { - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.blank), - ), - ); - } - } - }, - ); - }, - ); - } - - late WorkspaceService _workspaceService; - WorkspaceSectionsListener? _listener; - - @override - Future close() async { - await _listener?.stop(); - _listener = null; - return super.close(); - } - - ViewSectionPB? getViewSection(ViewPB view) { - final publicViews = state.section.publicViews.map((e) => e.id); - final privateViews = state.section.privateViews.map((e) => e.id); - if (publicViews.contains(view.id)) { - return ViewSectionPB.Public; - } else if (privateViews.contains(view.id)) { - return ViewSectionPB.Private; - } else { - return null; - } - } - - Future _getSectionViews() async { - try { - final publicViews = await _workspaceService.getPublicViews().getOrThrow(); - final privateViews = - await _workspaceService.getPrivateViews().getOrThrow(); - return SidebarSection( - publicViews: publicViews, - privateViews: privateViews, - ); - } catch (e) { - Log.error('Failed to get section views: $e'); - return null; - } - } - - 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, - ); - - _listener = WorkspaceSectionsListener( - user: userProfile, - workspaceId: workspaceId, - )..start( - sectionChanged: (result) { - if (!isClosed) { - result.fold( - (s) => add(SidebarSectionsEvent.receiveSectionViewsUpdate(s)), - (f) => Log.error('Failed to receive section views: $f'), - ); - } - }, - ); - } - - void _reset(UserProfilePB userProfile, String workspaceId) { - _listener?.stop(); - _listener = null; - - _initial(userProfile, workspaceId); - } -} - -@freezed -class SidebarSectionsEvent with _$SidebarSectionsEvent { - const factory SidebarSectionsEvent.initial( - UserProfilePB userProfile, - String workspaceId, - ) = _Initial; - const factory SidebarSectionsEvent.reset( - UserProfilePB userProfile, - String workspaceId, - ) = _Reset; - const factory SidebarSectionsEvent.createRootViewInSection({ - required String name, - required ViewSectionPB viewSection, - int? index, - }) = _CreateRootViewInSection; - const factory SidebarSectionsEvent.moveRootView({ - required int fromIndex, - required int toIndex, - required ViewSectionPB fromSection, - required ViewSectionPB toSection, - }) = _MoveRootView; - const factory SidebarSectionsEvent.receiveSectionViewsUpdate( - SectionViewsPB sectionViews, - ) = _ReceiveSectionViewsUpdate; - const factory SidebarSectionsEvent.reload( - UserProfilePB userProfile, - String workspaceId, - ) = _Reload; -} - -@freezed -class SidebarSectionsState with _$SidebarSectionsState { - const factory SidebarSectionsState({ - required SidebarSection section, - @Default(null) ViewPB? lastCreatedRootView, - FlowyResult? createRootViewResult, - @Default(true) bool containsSpace, - }) = _SidebarSectionsState; - - factory SidebarSectionsState.initial() => const SidebarSectionsState( - section: SidebarSection.empty(), - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart deleted file mode 100644 index 3f9657c5cf..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart +++ /dev/null @@ -1,48 +0,0 @@ -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'; - -/// Manages Local Notifications -/// -/// Currently supports: -/// - MacOS -/// - Windows -/// - Linux -/// -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, - ); - } -} - -/// Creates and shows a Notification -/// -class NotificationMessage { - NotificationMessage({ - required String title, - required String body, - String? identifier, - VoidCallback? onClick, - }) { - _notification = LocalNotification( - identifier: identifier, - title: title, - body: body, - )..onClick = onClick; - - _show(); - } - - late final LocalNotification _notification; - - void _show() => _notification.show(); -} 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 deleted file mode 100644 index a5381ce17f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart +++ /dev/null @@ -1,122 +0,0 @@ -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'; -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. -/// -/// Use-cases: -/// - Desktop: Command Palette recent view history -/// - Desktop: (Documents) Inline-page reference recent view history -/// - Mobile: Recent view history on home screen -/// -/// See the related [LaunchTask] in [RecentServiceTask]. -/// -class CachedRecentService { - CachedRecentService(); - - Completer _completer = Completer(); - - ValueNotifier> notifier = ValueNotifier(const []); - - List get _recentViews => notifier.value; - set _recentViews(List value) => notifier.value = value; - - final _listener = RecentViewsListener(); - - Future> recentViews() async { - if (_isInitialized || _completer.isCompleted) return _recentViews; - - _isInitialized = true; - - _listener.start(recentViewsUpdated: _recentViewsUpdated); - _recentViews = await _readRecentViews().fold( - (s) => s.items.unique((e) => e.item.id), - (_) => [], - ); - _completer.complete(); - - return _recentViews; - } - - /// Updates the recent views history - Future> updateRecentViews( - List viewIds, - bool addInRecent, - ) async { - final List duplicatedViewIds = []; - for (final viewId in viewIds) { - for (final view in _recentViews) { - if (view.item.id == viewId) { - duplicatedViewIds.add(viewId); - } - } - } - return FolderEventUpdateRecentViews( - UpdateRecentViewPayloadPB( - viewIds: addInRecent ? viewIds : duplicatedViewIds, - addInRecent: addInRecent, - ), - ).send(); - } - - Future> - _readRecentViews() async { - final payload = ReadRecentViewsPB(start: Int64(), limit: Int64(100)); - final result = await FolderEventReadRecentViews(payload).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, - ), - ), - ); - }, - (error) => FlowyResult.failure(error), - ); - } - - bool _isInitialized = false; - - Future reset() async { - await _listener.stop(); - _resetCompleter(); - _isInitialized = false; - _recentViews = const []; - } - - Future dispose() async { - notifier.dispose(); - await _listener.stop(); - } - - void _recentViewsUpdated( - FlowyResult result, - ) async { - final viewIds = result.toNullable(); - if (viewIds != null) { - _recentViews = await _readRecentViews().fold( - (s) => s.items.unique((e) => e.item.id), - (_) => [], - ); - } - } - - void _resetCompleter() { - if (!_completer.isCompleted) { - _completer.complete(); - } - _completer = Completer(); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/prelude.dart deleted file mode 100644 index 5266a440c8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/prelude.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'cached_recent_service.dart'; -export 'recent_views_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_listener.dart deleted file mode 100644 index fc70ef0602..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_listener.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/core/notification/folder_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.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'; -import 'package:flutter/foundation.dart'; - -typedef RecentViewsUpdated = void Function( - FlowyResult result, -); - -class RecentViewsListener { - StreamSubscription? _streamSubscription; - FolderNotificationParser? _parser; - - RecentViewsUpdated? _recentViewsUpdated; - - void start({ - RecentViewsUpdated? recentViewsUpdated, - }) { - _recentViewsUpdated = recentViewsUpdated; - _parser = FolderNotificationParser( - id: 'recent_views', - callback: _observableCallback, - ); - _streamSubscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - void _observableCallback( - FolderNotification ty, - FlowyResult result, - ) { - if (_recentViewsUpdated == null) { - return; - } - - result.fold( - (payload) { - final view = RepeatedViewIdPB.fromBuffer(payload); - _recentViewsUpdated?.call( - FlowyResult.success(view), - ); - }, - (error) => _recentViewsUpdated?.call( - FlowyResult.failure(error), - ), - ); - } - - Future stop() async { - _parser = null; - await _streamSubscription?.cancel(); - _recentViewsUpdated = null; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart deleted file mode 100644 index d67c24e854..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/recent/cached_recent_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'; - -part 'recent_views_bloc.freezed.dart'; - -class RecentViewsBloc extends Bloc { - RecentViewsBloc() : super(RecentViewsState.initial()) { - _service = getIt(); - _dispatch(); - } - - late final CachedRecentService _service; - - @override - Future close() async { - _service.notifier.removeListener(_onRecentViewsUpdated); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.map( - initial: (e) async { - _service.notifier.addListener(_onRecentViewsUpdated); - add(const RecentViewsEvent.fetchRecentViews()); - }, - addRecentViews: (e) async { - await _service.updateRecentViews(e.viewIds, true); - }, - removeRecentViews: (e) async { - await _service.updateRecentViews(e.viewIds, false); - }, - fetchRecentViews: (e) async { - emit( - state.copyWith( - isLoading: false, - views: await _service.recentViews(), - ), - ); - }, - resetRecentViews: (e) async { - await _service.reset(); - add(const RecentViewsEvent.fetchRecentViews()); - }, - ); - }, - ); - } - - void _onRecentViewsUpdated() => - add(const RecentViewsEvent.fetchRecentViews()); -} - -@freezed -class RecentViewsEvent with _$RecentViewsEvent { - const factory RecentViewsEvent.initial() = Initial; - const factory RecentViewsEvent.addRecentViews(List viewIds) = - AddRecentViews; - const factory RecentViewsEvent.removeRecentViews(List viewIds) = - RemoveRecentViews; - const factory RecentViewsEvent.fetchRecentViews() = FetchRecentViews; - const factory RecentViewsEvent.resetRecentViews() = ResetRecentViews; -} - -@freezed -class RecentViewsState with _$RecentViewsState { - const factory RecentViewsState({ - required List views, - @Default(true) bool isLoading, - }) = _RecentViewsState; - - factory RecentViewsState.initial() => const RecentViewsState(views: []); -} 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 a90f319a94..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ /dev/null @@ -1,128 +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 { - 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) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); - }, - Log.error, - ); - }, - restart: () async { - emit(LocalAiPluginState.loading()); - await AIEventRestartLocalAI().send(); - }, - ); - } - - void _startListening() { - listener.start( - stateCallback: (pluginState) { - add(LocalAiPluginEvent.didReceiveAiState(pluginState)); - }, - resourceCallback: (data) { - add(LocalAiPluginEvent.didReceiveLackOfResources(data)); - }, - ); - } - - void _getLocalAiState() { - AIEventGetLocalAIState().send().fold( - (aiState) { - 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 deleted file mode 100644 index 99b9eaa2c9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart +++ /dev/null @@ -1,449 +0,0 @@ -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'; -import 'package:appflowy_editor/appflowy_editor.dart' - show AppFlowyEditorLocalizations; -import 'package:easy_localization/easy_localization.dart'; -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'; - -/// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy. -/// It includes: -/// - [AppTheme] -/// - [ThemeMode] -/// - [TextStyle]'s -/// - [Locale] -/// - [UserDateFormatPB] -/// - [UserTimeFormatPB] -/// -class AppearanceSettingsCubit extends Cubit { - AppearanceSettingsCubit( - AppearanceSettingsPB appearanceSettings, - DateTimeSettingsPB dateTimeSettings, - AppTheme appTheme, - ) : _appearanceSettings = appearanceSettings, - _dateTimeSettings = dateTimeSettings, - super( - AppearanceSettingsState.initial( - appTheme, - appearanceSettings.themeMode, - appearanceSettings.font, - appearanceSettings.layoutDirection, - appearanceSettings.textDirection, - appearanceSettings.enableRtlToolbarItems, - appearanceSettings.locale, - appearanceSettings.isMenuCollapsed, - appearanceSettings.menuOffset, - dateTimeSettings.dateFormat, - dateTimeSettings.timeFormat, - dateTimeSettings.timezoneId, - appearanceSettings.documentSetting.cursorColor.isEmpty - ? null - : Color( - int.parse(appearanceSettings.documentSetting.cursorColor), - ), - appearanceSettings.documentSetting.selectionColor.isEmpty - ? null - : Color( - int.parse( - appearanceSettings.documentSetting.selectionColor, - ), - ), - 1.0, - ), - ) { - readTextScaleFactor(); - } - - final AppearanceSettingsPB _appearanceSettings; - final DateTimeSettingsPB _dateTimeSettings; - - Future setTextScaleFactor(double textScaleFactor) async { - // only saved in local storage, this value is not synced across devices - await getIt().set( - KVKeys.textScaleFactor, - textScaleFactor.toString(), - ); - - // don't allow the text scale factor to be greater than 1.0, it will cause - // ui issues - emit(state.copyWith(textScaleFactor: textScaleFactor.clamp(0.7, 1.0))); - } - - Future readTextScaleFactor() async { - final textScaleFactor = await getIt().getWithFormat( - KVKeys.textScaleFactor, - (value) => double.parse(value), - ) ?? - 1.0; - emit(state.copyWith(textScaleFactor: textScaleFactor.clamp(0.7, 1.0))); - } - - /// Update selected theme in the user's settings and emit an updated state - /// with the AppTheme named [themeName]. - 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, - ); - } - } - } - - /// Reset the current user selected theme back to the default - Future resetTheme() => - setTheme(DefaultAppearanceSettings.kDefaultThemeName); - - /// Update the theme mode in the user's settings and emit an updated state. - void setThemeMode(ThemeMode themeMode) { - _appearanceSettings.themeMode = _themeModeToPB(themeMode); - _saveAppearanceSettings(); - emit(state.copyWith(themeMode: themeMode)); - } - - /// Resets the current brightness setting - void resetThemeMode() => - setThemeMode(DefaultAppearanceSettings.kDefaultThemeMode); - - /// Toggle the theme mode - void toggleThemeMode() { - final currentThemeMode = state.themeMode; - setThemeMode( - currentThemeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light, - ); - } - - void setLayoutDirection(LayoutDirection layoutDirection) { - _appearanceSettings.layoutDirection = layoutDirection.toLayoutDirectionPB(); - _saveAppearanceSettings(); - emit(state.copyWith(layoutDirection: layoutDirection)); - } - - void setTextDirection(AppFlowyTextDirection textDirection) { - _appearanceSettings.textDirection = textDirection.toTextDirectionPB(); - _saveAppearanceSettings(); - emit(state.copyWith(textDirection: textDirection)); - } - - void setEnableRTLToolbarItems(bool value) { - _appearanceSettings.enableRtlToolbarItems = value; - _saveAppearanceSettings(); - emit(state.copyWith(enableRtlToolbarItems: value)); - } - - /// Update selected font in the user's settings and emit an updated state - /// with the font name. - void setFontFamily(String fontFamilyName) { - _appearanceSettings.font = fontFamilyName; - _saveAppearanceSettings(); - emit(state.copyWith(font: fontFamilyName)); - } - - /// Resets the current font family for the user preferences - void resetFontFamily() => - setFontFamily(DefaultAppearanceSettings.kDefaultFontFamily); - - /// Update document cursor color in the appearance settings and emit an updated state. - void setDocumentCursorColor(Color color) { - _appearanceSettings.documentSetting.cursorColor = color.toHexString(); - _saveAppearanceSettings(); - emit(state.copyWith(documentCursorColor: color)); - } - - /// Reset document cursor color in the appearance settings - void resetDocumentCursorColor() { - _appearanceSettings.documentSetting.cursorColor = ''; - _saveAppearanceSettings(); - emit(state.copyWith(documentCursorColor: null)); - } - - /// Update document selection color in the appearance settings and emit an updated state. - void setDocumentSelectionColor(Color color) { - _appearanceSettings.documentSetting.selectionColor = color.toHexString(); - _saveAppearanceSettings(); - emit(state.copyWith(documentSelectionColor: color)); - } - - /// Reset document selection color in the appearance settings - void resetDocumentSelectionColor() { - _appearanceSettings.documentSetting.selectionColor = ''; - _saveAppearanceSettings(); - emit(state.copyWith(documentSelectionColor: null)); - } - - /// Updates the current locale and notify the listeners the locale was - /// changed. Fallback to [en] locale if [newLocale] is not supported. - void setLocale(BuildContext context, Locale newLocale) { - if (!context.supportedLocales.contains(newLocale)) { - // Log.warn("Unsupported locale: $newLocale, Fallback to locale: en"); - newLocale = const Locale('en'); - } - - context.setLocale(newLocale).catchError((e) { - Log.warn('Catch error in setLocale: $e}'); - }); - - // Sync the app's locale with the editor (initialization and update) - AppFlowyEditorLocalizations.load(newLocale); - - if (state.locale != newLocale) { - _appearanceSettings.locale.languageCode = newLocale.languageCode; - _appearanceSettings.locale.countryCode = newLocale.countryCode ?? ""; - _saveAppearanceSettings(); - emit(state.copyWith(locale: newLocale)); - } - } - - // Saves the menus current visibility - void saveIsMenuCollapsed(bool collapsed) { - _appearanceSettings.isMenuCollapsed = collapsed; - _saveAppearanceSettings(); - } - - // Saves the current resize offset of the menu - void saveMenuOffset(double offset) { - _appearanceSettings.menuOffset = offset; - _saveAppearanceSettings(); - } - - /// Saves key/value setting to disk. - /// Removes the key if the passed in value is null - void setKeyValue(String key, String? value) { - if (key.isEmpty) { - Log.warn("The key should not be empty"); - return; - } - - if (value == null) { - _appearanceSettings.settingKeyValue.remove(key); - } - - if (_appearanceSettings.settingKeyValue[key] != value) { - if (value == null) { - _appearanceSettings.settingKeyValue.remove(key); - } else { - _appearanceSettings.settingKeyValue[key] = value; - } - } - _saveAppearanceSettings(); - } - - String? getValue(String key) { - if (key.isEmpty) { - Log.warn("The key should not be empty"); - return null; - } - return _appearanceSettings.settingKeyValue[key]; - } - - /// Called when the application launches. - /// Uses the device locale when the application is opened for the first time. - void readLocaleWhenAppLaunch(BuildContext context) { - if (_appearanceSettings.resetToDefault) { - _appearanceSettings.resetToDefault = false; - _saveAppearanceSettings(); - setLocale(context, context.deviceLocale); - return; - } - - setLocale(context, state.locale); - } - - void setDateFormat(UserDateFormatPB format) { - _dateTimeSettings.dateFormat = format; - _saveDateTimeSettings(); - emit(state.copyWith(dateFormat: format)); - } - - void setTimeFormat(UserTimeFormatPB format) { - _dateTimeSettings.timeFormat = format; - _saveDateTimeSettings(); - emit(state.copyWith(timeFormat: format)); - } - - Future _saveDateTimeSettings() async { - final result = await UserSettingsBackendService() - .setDateTimeSettings(_dateTimeSettings); - result.fold( - (_) => null, - (error) => Log.error(error), - ); - } - - Future _saveAppearanceSettings() async { - final result = await UserSettingsBackendService() - .setAppearanceSetting(_appearanceSettings); - result.fold( - (l) => null, - (error) => Log.error(error), - ); - } -} - -ThemeMode _themeModeFromPB(ThemeModePB themeModePB) { - switch (themeModePB) { - case ThemeModePB.Light: - return ThemeMode.light; - case ThemeModePB.Dark: - return ThemeMode.dark; - case ThemeModePB.System: - default: - return ThemeMode.system; - } -} - -ThemeModePB _themeModeToPB(ThemeMode themeMode) { - switch (themeMode) { - case ThemeMode.light: - return ThemeModePB.Light; - case ThemeMode.dark: - return ThemeModePB.Dark; - case ThemeMode.system: - return ThemeModePB.System; - } -} - -enum LayoutDirection { - ltrLayout, - rtlLayout; - - static LayoutDirection fromLayoutDirectionPB( - LayoutDirectionPB layoutDirectionPB, - ) => - layoutDirectionPB == LayoutDirectionPB.RTLLayout - ? LayoutDirection.rtlLayout - : LayoutDirection.ltrLayout; - - LayoutDirectionPB toLayoutDirectionPB() => this == LayoutDirection.rtlLayout - ? LayoutDirectionPB.RTLLayout - : LayoutDirectionPB.LTRLayout; -} - -enum AppFlowyTextDirection { - ltr, - rtl, - auto; - - static AppFlowyTextDirection fromTextDirectionPB( - TextDirectionPB? textDirectionPB, - ) { - switch (textDirectionPB) { - case TextDirectionPB.LTR: - return AppFlowyTextDirection.ltr; - case TextDirectionPB.RTL: - return AppFlowyTextDirection.rtl; - case TextDirectionPB.AUTO: - return AppFlowyTextDirection.auto; - default: - return AppFlowyTextDirection.ltr; - } - } - - TextDirectionPB toTextDirectionPB() { - switch (this) { - case AppFlowyTextDirection.ltr: - return TextDirectionPB.LTR; - case AppFlowyTextDirection.rtl: - return TextDirectionPB.RTL; - case AppFlowyTextDirection.auto: - return TextDirectionPB.AUTO; - } - } -} - -@freezed -class AppearanceSettingsState with _$AppearanceSettingsState { - const AppearanceSettingsState._(); - - const factory AppearanceSettingsState({ - required AppTheme appTheme, - required ThemeMode themeMode, - required String font, - required LayoutDirection layoutDirection, - required AppFlowyTextDirection textDirection, - required bool enableRtlToolbarItems, - required Locale locale, - required bool isMenuCollapsed, - required double menuOffset, - required UserDateFormatPB dateFormat, - required UserTimeFormatPB timeFormat, - required String timezoneId, - required Color? documentCursorColor, - required Color? documentSelectionColor, - required double textScaleFactor, - }) = _AppearanceSettingsState; - - factory AppearanceSettingsState.initial( - AppTheme appTheme, - ThemeModePB themeModePB, - String font, - LayoutDirectionPB layoutDirectionPB, - TextDirectionPB? textDirectionPB, - bool enableRtlToolbarItems, - LocaleSettingsPB localePB, - bool isMenuCollapsed, - double menuOffset, - UserDateFormatPB dateFormat, - UserTimeFormatPB timeFormat, - String timezoneId, - Color? documentCursorColor, - Color? documentSelectionColor, - double textScaleFactor, - ) { - return AppearanceSettingsState( - appTheme: appTheme, - font: font, - layoutDirection: LayoutDirection.fromLayoutDirectionPB(layoutDirectionPB), - textDirection: AppFlowyTextDirection.fromTextDirectionPB(textDirectionPB), - enableRtlToolbarItems: enableRtlToolbarItems, - themeMode: _themeModeFromPB(themeModePB), - locale: Locale(localePB.languageCode, localePB.countryCode), - isMenuCollapsed: isMenuCollapsed, - menuOffset: menuOffset, - dateFormat: dateFormat, - timeFormat: timeFormat, - timezoneId: timezoneId, - documentCursorColor: documentCursorColor, - documentSelectionColor: documentSelectionColor, - textScaleFactor: textScaleFactor, - ); - } - - ThemeData get lightTheme => _getThemeData(Brightness.light); - - ThemeData get darkTheme => _getThemeData(Brightness.dark); - - ThemeData _getThemeData(Brightness brightness) { - return getIt().getThemeData( - appTheme, - brightness, - font, - builtInCodeFontFamily, - ); - } -} 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 deleted file mode 100644 index 6be53cf158..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme.dart'; -import 'package:flutter/material.dart'; - -// the default font family is empty, so we can use the default font family of the platform -// the system will choose the default font family of the platform -// iOS: San Francisco -// Android: Roboto -// Desktop: Based on the OS -const defaultFontFamily = ''; - -const builtInCodeFontFamily = 'RobotoMono'; - -abstract class BaseAppearance { - final white = const Color(0xFFFFFFFF); - - final Set scrollbarInteractiveStates = { - WidgetState.pressed, - WidgetState.hovered, - WidgetState.dragged, - }; - - TextStyle getFontStyle({ - required String fontFamily, - double? fontSize, - FontWeight? fontWeight, - Color? fontColor, - double? letterSpacing, - double? lineHeight, - }) { - fontSize = fontSize ?? FontSizes.s14; - fontWeight = fontWeight ?? FontWeight.w400; - letterSpacing = fontSize * (letterSpacing ?? 0.005); - - final textStyle = TextStyle( - fontFamily: fontFamily.isEmpty ? null : fontFamily, - fontSize: fontSize, - color: fontColor, - fontWeight: fontWeight, - letterSpacing: letterSpacing, - height: lineHeight, - ); - - if (fontFamily == defaultFontFamily) { - return textStyle; - } - - try { - return getGoogleFontSafely( - fontFamily, - fontSize: fontSize, - fontColor: fontColor, - fontWeight: fontWeight, - letterSpacing: letterSpacing, - lineHeight: lineHeight, - ); - } catch (e) { - return textStyle; - } - } - - TextTheme getTextTheme({ - required String fontFamily, - required Color fontColor, - }) { - return TextTheme( - displayLarge: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s32, - fontColor: fontColor, - fontWeight: FontWeight.w600, - lineHeight: 42.0, - ), // h2 - displayMedium: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s24, - fontColor: fontColor, - fontWeight: FontWeight.w600, - lineHeight: 34.0, - ), // h3 - displaySmall: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s20, - fontColor: fontColor, - fontWeight: FontWeight.w600, - lineHeight: 28.0, - ), // h4 - titleLarge: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s18, - fontColor: fontColor, - fontWeight: FontWeight.w600, - ), // title - titleMedium: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s16, - fontColor: fontColor, - fontWeight: FontWeight.w600, - ), // heading - titleSmall: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s14, - fontColor: fontColor, - fontWeight: FontWeight.w600, - ), // subheading - bodyMedium: getFontStyle( - fontFamily: fontFamily, - fontColor: fontColor, - ), // body-regular - bodySmall: getFontStyle( - fontFamily: fontFamily, - fontColor: fontColor, - fontWeight: FontWeight.w400, - ), // body-thin - ); - } - - ThemeData getThemeData( - AppTheme appTheme, - Brightness brightness, - String fontFamily, - String codeFontFamily, - ); -} 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 deleted file mode 100644 index c1e539cf58..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ /dev/null @@ -1,159 +0,0 @@ -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 - ThemeData getThemeData( - AppTheme appTheme, - Brightness brightness, - String fontFamily, - String codeFontFamily, - ) { - assert(codeFontFamily.isNotEmpty); - - fontFamily = fontFamily.isEmpty ? defaultFontFamily : fontFamily; - - final isLight = brightness == Brightness.light; - final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; - - final colorScheme = ColorScheme( - brightness: brightness, - primary: theme.primary, - onPrimary: theme.onPrimary, - primaryContainer: theme.main2, - onPrimaryContainer: white, - // page title hover color - secondary: theme.hoverBG1, - onSecondary: theme.shader1, - // setting value hover color - secondaryContainer: theme.selector, - onSecondaryContainer: theme.topbarBg, - tertiary: theme.shader7, - // Editor: toolbarColor - onTertiary: theme.toolbarColor, - tertiaryContainer: theme.questionBubbleBG, - surface: theme.surface, - // text&icon color when it is hovered - onSurface: theme.hoverFG, - // grey hover color - inverseSurface: theme.hoverBG3, - onError: theme.onPrimary, - error: theme.red, - outline: theme.shader4, - surfaceContainerHighest: theme.sidebarBg, - shadow: theme.shadow, - ); - - // 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, - textTheme: getTextTheme( - fontFamily: fontFamily, - fontColor: theme.text, - ), - textButtonTheme: const TextButtonThemeData( - style: ButtonStyle( - minimumSize: WidgetStatePropertyAll(Size.zero), - ), - ), - textSelectionTheme: TextSelectionThemeData( - cursorColor: theme.main2, - selectionHandleColor: theme.main2, - ), - iconTheme: IconThemeData(color: theme.icon), - tooltipTheme: TooltipThemeData( - textStyle: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s11, - fontWeight: FontWeight.w400, - fontColor: theme.surface, - ), - ), - scaffoldBackgroundColor: theme.surface, - snackBarTheme: SnackBarThemeData( - backgroundColor: colorScheme.primary, - contentTextStyle: TextStyle(color: colorScheme.onSurface), - ), - scrollbarTheme: ScrollbarThemeData( - thumbColor: WidgetStateProperty.resolveWith( - (states) => states.any(scrollbarInteractiveStates.contains) - ? theme.scrollbarHoverColor - : theme.scrollbarColor, - ), - thickness: WidgetStateProperty.resolveWith((_) => 4.0), - crossAxisMargin: 0.0, - mainAxisMargin: 6.0, - radius: Corners.s10Radius, - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - //dropdown menu color - canvasColor: theme.surface, - dividerColor: theme.divider, - hintColor: theme.hint, - //action item hover color - hoverColor: theme.hoverBG2, - disabledColor: theme.shader4, - highlightColor: theme.main1, - indicatorColor: theme.main1, - cardColor: theme.input, - colorScheme: colorScheme, - - extensions: [ - AFThemeExtension( - warning: theme.yellow, - success: theme.green, - tint1: theme.tint1, - tint2: theme.tint2, - tint3: theme.tint3, - tint4: theme.tint4, - tint5: theme.tint5, - tint6: theme.tint6, - tint7: theme.tint7, - tint8: theme.tint8, - tint9: theme.tint9, - textColor: theme.text, - secondaryTextColor: theme.secondaryText, - strongText: theme.strongText, - greyHover: theme.hoverBG1, - greySelect: theme.bg3, - lightGreyHover: theme.hoverBG3, - toggleOffFill: theme.shader5, - progressBarBGColor: theme.progressBarBGColor, - toggleButtonBGColor: theme.toggleButtonBGColor, - calendarWeekendBGColor: theme.calendarWeekendBGColor, - gridRowCountColor: theme.gridRowCountColor, - code: getFontStyle( - fontFamily: codeFontFamily, - fontColor: theme.shader3, - ), - callout: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s11, - fontColor: theme.shader3, - ), - calloutBGColor: theme.hoverBG3, - tableCellBGColor: theme.surface, - caption: getFontStyle( - fontFamily: fontFamily, - fontSize: FontSizes.s11, - fontWeight: FontWeight.w400, - fontColor: theme.hint, - ), - 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 deleted file mode 100644 index 46eddd53ab..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ /dev/null @@ -1,284 +0,0 @@ -// 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 - static const _onBackgroundColor = Color(0xff2F3030); // text/title color - static const _onSurfaceColor = Color(0xff676666); // text/body color - static const _onSecondaryColor = Color(0xFFC5C7CB); // text/body2 color - static const _hintColorInDarkMode = Color(0xff626262); // hint color - - @override - ThemeData getThemeData( - AppTheme appTheme, - Brightness brightness, - String fontFamily, - String codeFontFamily, - ) { - assert(codeFontFamily.isNotEmpty); - - final fontStyle = getFontStyle( - fontFamily: fontFamily, - fontSize: 16.0, - fontWeight: FontWeight.w400, - ); - - final isLight = brightness == Brightness.light; - final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); - - final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; - - final colorTheme = isLight - ? ColorScheme( - brightness: brightness, - primary: _primaryColor, - onPrimary: Colors.white, - // group card header background color - primaryContainer: const Color(0xffF1F1F4), // primary 20 - // group card & property edit background color - secondary: const Color(0xfff7f8fc), // shade 10 - onSecondary: _onSecondaryColor, - // hidden group title & card text color - tertiary: const Color(0xff858585), // for light text - error: const Color(0xffFB006D), - onError: const Color(0xffFB006D), - outline: const Color(0xffe3e3e3), - outlineVariant: const Color(0xffCBD5E0).withValues(alpha: 0.24), - //Snack bar - surface: Colors.white, - onSurface: _onSurfaceColor, // text/body color - surfaceContainerHighest: theme.sidebarBg, - ) - : ColorScheme( - brightness: brightness, - primary: _primaryColor, - onPrimary: Colors.black, - secondary: const Color(0xff2d2d2d), //temp - onSecondary: Colors.white, - tertiary: const Color(0xff858585), // temp - error: const Color(0xffFB006D), - onError: const Color(0xffFB006D), - outline: _hintColorInDarkMode, - outlineVariant: Colors.black, - //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); - - return ThemeData( - useMaterial3: false, - primaryColor: colorTheme.primary, //primary 100 - primaryColorLight: const Color(0xFF57B5F8), //primary 80 - dividerColor: colorTheme.outline, //caption - hintColor: hintColor, - disabledColor: colorTheme.outline, - scaffoldBackgroundColor: background, - appBarTheme: AppBarTheme( - toolbarHeight: 44.0, - foregroundColor: onBackground, - backgroundColor: background, - centerTitle: false, - titleTextStyle: TextStyle( - color: onBackground, - fontSize: 18, - fontWeight: FontWeight.w600, - letterSpacing: 0.05, - ), - shadowColor: colorTheme.outlineVariant, - ), - radioTheme: RadioThemeData( - fillColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return colorTheme.primary; - } - return colorTheme.outline; - }), - ), - // button - elevatedButtonTheme: ElevatedButtonThemeData( - style: ButtonStyle( - fixedSize: WidgetStateProperty.all(const Size.fromHeight(48)), - elevation: WidgetStateProperty.all(0), - textStyle: WidgetStateProperty.all( - TextStyle( - fontSize: 14, - fontFamily: fontStyle.fontFamily, - fontWeight: FontWeight.w600, - ), - ), - shadowColor: WidgetStateProperty.all(null), - foregroundColor: WidgetStateProperty.all(Colors.white), - backgroundColor: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.disabled)) { - return _primaryColor; - } - return colorTheme.primary; - }, - ), - ), - ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: ButtonStyle( - textStyle: WidgetStateProperty.all( - TextStyle( - fontSize: 14, - fontFamily: fontStyle.fontFamily, - fontWeight: FontWeight.w500, - ), - ), - foregroundColor: WidgetStateProperty.all(onBackground), - backgroundColor: WidgetStateProperty.all(background), - shape: WidgetStateProperty.all( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), - ), - side: WidgetStateProperty.all( - BorderSide(color: colorTheme.outline, width: 0.5), - ), - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(horizontal: 8, vertical: 12), - ), - ), - ), - textButtonTheme: TextButtonThemeData( - style: ButtonStyle( - textStyle: WidgetStateProperty.all(fontStyle), - ), - ), - // text - fontFamily: fontStyle.fontFamily, - textTheme: TextTheme( - displayLarge: const TextStyle( - color: _primaryColor, - fontSize: 32, - fontWeight: FontWeight.w700, - height: 1.20, - letterSpacing: 0.16, - ), - displayMedium: fontStyle.copyWith( - color: onBackground, - fontSize: 32, - fontWeight: FontWeight.w600, - height: 1.20, - letterSpacing: 0.16, - ), - // H1 Semi 26 - displaySmall: fontStyle.copyWith( - color: onBackground, - fontWeight: FontWeight.w600, - height: 1.10, - letterSpacing: 0.13, - ), - // body2 14 Regular - bodyMedium: fontStyle.copyWith( - color: onBackground, - fontWeight: FontWeight.w400, - letterSpacing: 0.07, - ), - // Trash empty title - labelLarge: fontStyle.copyWith( - color: onBackground, - fontSize: 22, - fontWeight: FontWeight.w600, - letterSpacing: -0.3, - ), - // setting item title - labelMedium: fontStyle.copyWith( - color: onBackground, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - // setting group title - labelSmall: fontStyle.copyWith( - color: onBackground, - fontSize: 16, - fontWeight: FontWeight.w600, - letterSpacing: 0.05, - ), - ), - inputDecorationTheme: InputDecorationTheme( - contentPadding: const EdgeInsets.all(8), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide( - width: 2, - color: _primaryColor, - ), - borderRadius: BorderRadius.all(Radius.circular(6)), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorTheme.error), - borderRadius: const BorderRadius.all(Radius.circular(6)), - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorTheme.error), - borderRadius: const BorderRadius.all(Radius.circular(6)), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: colorTheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(6)), - ), - ), - colorScheme: colorTheme, - indicatorColor: Colors.blue, - extensions: [ - AFThemeExtension( - warning: theme.yellow, - success: theme.green, - tint1: theme.tint1, - tint2: theme.tint2, - tint3: theme.tint3, - tint4: theme.tint4, - tint5: theme.tint5, - tint6: theme.tint6, - tint7: theme.tint7, - tint8: theme.tint8, - tint9: theme.tint9, - textColor: theme.text, - secondaryTextColor: theme.secondaryText, - strongText: theme.strongText, - greyHover: theme.hoverBG1, - greySelect: theme.bg3, - lightGreyHover: theme.hoverBG3, - toggleOffFill: theme.shader5, - progressBarBGColor: theme.progressBarBGColor, - toggleButtonBGColor: theme.toggleButtonBGColor, - calendarWeekendBGColor: theme.calendarWeekendBGColor, - gridRowCountColor: theme.gridRowCountColor, - code: codeFontStyle.copyWith( - color: theme.shader3, - ), - callout: fontStyle.copyWith( - fontSize: FontSizes.s11, - color: theme.shader3, - ), - calloutBGColor: theme.hoverBG3, - tableCellBGColor: theme.surface, - caption: fontStyle.copyWith( - fontSize: FontSizes.s11, - fontWeight: FontWeight.w400, - color: theme.hint, - ), - 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 deleted file mode 100644 index febb89727a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_setting_bloc.dart +++ /dev/null @@ -1,119 +0,0 @@ -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'; -import 'package:appflowy_backend/log.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:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'appflowy_cloud_setting_bloc.freezed.dart'; - -class AppFlowyCloudSettingBloc - extends Bloc { - AppFlowyCloudSettingBloc(CloudSettingPB setting) - : _listener = UserCloudConfigListener(), - super(AppFlowyCloudSettingState.initial(setting, false)) { - _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 { - await getSyncLogEnabled().then((value) { - emit(state.copyWith(isSyncLogEnabled: value)); - }); - - _listener.start( - onSettingChanged: (result) { - if (isClosed) { - return; - } - result.fold( - (setting) => - add(AppFlowyCloudSettingEvent.didReceiveSetting(setting)), - (error) => Log.error(error), - ); - }, - ); - }, - enableSync: (isEnable) async { - 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( - setting: setting, - showRestartHint: setting.serverUrl.isNotEmpty, - ), - ); - }, - ); - }, - ); - } -} - -@freezed -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; -} - -@freezed -class AppFlowyCloudSettingState with _$AppFlowyCloudSettingState { - const factory AppFlowyCloudSettingState({ - required CloudSettingPB setting, - required bool showRestartHint, - required bool isSyncLogEnabled, - }) = _AppFlowyCloudSettingState; - - factory AppFlowyCloudSettingState.initial( - CloudSettingPB setting, - bool isSyncLogEnabled, - ) => - AppFlowyCloudSettingState( - setting: setting, - showRestartHint: setting.serverUrl.isNotEmpty, - isSyncLogEnabled: isSyncLogEnabled, - ); -} - -FlowyResult validateUrl(String url) { - try { - // Use Uri.parse to validate the url. - final uri = Uri.parse(url); - if (uri.isScheme('HTTP') || uri.isScheme('HTTPS')) { - return FlowyResult.success(null); - } else { - return FlowyResult.failure( - LocaleKeys.settings_menu_invalidCloudURLScheme.tr(), - ); - } - } catch (e) { - return FlowyResult.failure(e.toString()); - } -} 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 deleted file mode 100644 index 5652904180..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart +++ /dev/null @@ -1,141 +0,0 @@ -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_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'appflowy_cloud_urls_bloc.freezed.dart'; - -class AppFlowyCloudURLsBloc - extends Bloc { - AppFlowyCloudURLsBloc() : super(AppFlowyCloudURLsState.initial()) { - on((event, emit) async { - await event.when( - initial: () async {}, - updateServerUrl: (url) { - emit( - state.copyWith( - updatedServerUrl: url, - urlError: null, - showRestartHint: url.isNotEmpty, - ), - ); - }, - updateBaseWebDomain: (url) { - emit( - state.copyWith( - updatedBaseWebDomain: url, - urlError: null, - showRestartHint: url.isNotEmpty, - ), - ); - }, - confirmUpdate: () async { - if (state.updatedServerUrl.isEmpty) { - emit( - state.copyWith( - updatedServerUrl: "", - urlError: - LocaleKeys.settings_menu_appFlowyCloudUrlCanNotBeEmpty.tr(), - restartApp: false, - ), - ); - } else { - bool isSuccess = false; - - await validateUrl(state.updatedServerUrl).fold( - (url) async { - await useSelfHostedAppFlowyCloudWithURL(url); - isSuccess = true; - }, - (err) async => 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: () { - emit( - state.copyWith( - urlError: null, - restartApp: true, - ), - ); - }, - ); - }); - } -} - -@freezed -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; -} - -@freezed -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, - }) = _AppFlowyCloudURLsState; - - factory AppFlowyCloudURLsState.initial() => AppFlowyCloudURLsState( - config: getIt().appflowyCloudConfig, - urlError: null, - updatedServerUrl: - getIt().appflowyCloudConfig.base_url, - updatedBaseWebDomain: - getIt().appflowyCloudConfig.base_web_domain, - showRestartHint: getIt() - .appflowyCloudConfig - .base_url - .isNotEmpty, - restartApp: false, - ); -} - -FlowyResult validateUrl(String url) { - try { - // Use Uri.parse to validate the url. - final uri = Uri.parse(removeTrailingSlash(url)); - if (uri.isScheme('HTTP') || uri.isScheme('HTTPS')) { - return FlowyResult.success(uri.toString()); - } else { - return FlowyResult.failure( - LocaleKeys.settings_menu_invalidCloudURLScheme.tr(), - ); - } - } catch (e) { - return FlowyResult.failure(e.toString()); - } -} - -String removeTrailingSlash(String input) { - if (input.endsWith('/')) { - return input.substring(0, input.length - 1); - } - return input; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/application_data_storage.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/application_data_storage.dart deleted file mode 100644 index b0c8cb0948..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/application_data_storage.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; - -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:path/path.dart' as p; - -import '../../../startup/tasks/prelude.dart'; - -const appFlowyDataFolder = "AppFlowyDataDoNotRename"; - -class ApplicationDataStorage { - ApplicationDataStorage(); - String? _cachePath; - - /// Set the custom path to store the data. - /// If the path is not exists, the path will be created. - /// If the path is invalid, the path will be set to the default path. - Future setCustomPath(String path) async { - if (kIsWeb || Platform.isAndroid || Platform.isIOS) { - Log.info('LocalFileStorage is not supported on this platform.'); - return; - } - - if (Platform.isMacOS) { - // remove the prefix `/Volumes/*` - path = path.replaceFirst(macOSVolumesRegex, ''); - } else if (Platform.isWindows) { - path = path.replaceAll('/', '\\'); - } - - // If the path is not ends with `AppFlowyData`, we will append the - // `AppFlowyData` to the path. If the path is ends with `AppFlowyData`, - // which means the path is the custom path. - if (p.basename(path) != appFlowyDataFolder) { - path = p.join(path, appFlowyDataFolder); - } - - // create the directory if not exists. - final directory = Directory(path); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - - await setPath(path); - } - - Future setPath(String path) async { - if (kIsWeb || Platform.isAndroid || Platform.isIOS) { - Log.info('LocalFileStorage is not supported on this platform.'); - return; - } - - await getIt().set(KVKeys.pathLocation, path); - // clear the cache path, and not set the cache path to the new path because the set path may be invalid - _cachePath = null; - } - - Future getPath() async { - if (_cachePath != null) { - return _cachePath!; - } - - final response = await getIt().get(KVKeys.pathLocation); - - String path; - if (response == null) { - final directory = await appFlowyApplicationDataDirectory(); - path = directory.path; - } else { - path = response; - } - _cachePath = path; - - // if the path is not exists means the path is invalid, so we should clear the kv store - if (!Directory(path).existsSync()) { - await getIt().clear(); - final directory = await appFlowyApplicationDataDirectory(); - path = directory.path; - } - - return path; - } -} - -class MockApplicationDataStorage extends ApplicationDataStorage { - MockApplicationDataStorage(); - - // this value will be clear after setup - // only for the initial step - @visibleForTesting - static String? initialPath; - - @override - Future getPath() async { - final path = initialPath; - if (path != null) { - initialPath = null; - await super.setPath(path); - return Future.value(path); - } - return super.getPath(); - } -} 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 deleted file mode 100644 index df880891e9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart +++ /dev/null @@ -1,324 +0,0 @@ -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/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:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; - -part 'settings_billing_bloc.freezed.dart'; - -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); - - on((event, emit) async { - await event.when( - started: () async { - emit(const SettingsBillingState.loading()); - - FlowyError? error; - - final result = await UserBackendService.getWorkspaceSubscriptionInfo( - workspaceId, - ); - - final subscriptionInfo = result.fold( - (s) => s, - (e) { - error = e; - return null; - }, - ); - - if (subscriptionInfo == null || error != null) { - return emit(SettingsBillingState.error(error: error)); - } - - 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, - ); - 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; - } - - 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, - ), - ); - }, - 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._(); - - const factory SettingsBillingState.initial() = _Initial; - - const factory SettingsBillingState.loading() = _Loading; - - const factory SettingsBillingState.error({ - @Default(null) FlowyError? error, - }) = _Error; - - const factory SettingsBillingState.ready({ - required WorkspaceSubscriptionInfoPB subscriptionInfo, - 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/cloud_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/cloud_setting_bloc.dart deleted file mode 100644 index 9845b86d99..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/cloud_setting_bloc.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'cloud_setting_bloc.freezed.dart'; - -class CloudSettingBloc extends Bloc { - CloudSettingBloc(AuthenticatorType cloudType) - : super(CloudSettingState.initial(cloudType)) { - on((event, emit) async { - await event.when( - initial: () async {}, - updateCloudType: (AuthenticatorType newCloudType) async { - emit(state.copyWith(cloudType: newCloudType)); - }, - ); - }); - } -} - -@freezed -class CloudSettingEvent with _$CloudSettingEvent { - const factory CloudSettingEvent.initial() = _Initial; - const factory CloudSettingEvent.updateCloudType( - AuthenticatorType newCloudType, - ) = _UpdateCloudType; -} - -@freezed -class CloudSettingState with _$CloudSettingState { - const factory CloudSettingState({ - required AuthenticatorType cloudType, - }) = _CloudSettingState; - - factory CloudSettingState.initial(AuthenticatorType cloudType) => - CloudSettingState( - cloudType: cloudType, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/cloud_setting_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/cloud_setting_listener.dart deleted file mode 100644 index b6007847b3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/cloud_setting_listener.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -import '../../../core/notification/user_notification.dart'; - -class UserCloudConfigListener { - UserCloudConfigListener(); - - UserNotificationParser? _userParser; - StreamSubscription? _subscription; - void Function(FlowyResult)? _onSettingChanged; - - void start({ - void Function(FlowyResult)? onSettingChanged, - }) { - _onSettingChanged = onSettingChanged; - _userParser = UserNotificationParser( - id: 'user_cloud_config', - callback: _userNotificationCallback, - ); - _subscription = RustStreamReceiver.listen((observable) { - _userParser?.parse(observable); - }); - } - - Future stop() async { - _userParser = null; - await _subscription?.cancel(); - _onSettingChanged = null; - } - - void _userNotificationCallback( - UserNotification ty, - FlowyResult result, - ) { - switch (ty) { - case UserNotification.DidUpdateCloudConfig: - result.fold( - (payload) => _onSettingChanged - ?.call(FlowyResult.success(CloudSettingPB.fromBuffer(payload))), - (error) => _onSettingChanged?.call(FlowyResult.failure(error)), - ); - break; - default: - break; - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/create_file_settings_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/create_file_settings_cubit.dart deleted file mode 100644 index 7ff1ed5fed..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/create_file_settings_cubit.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class CreateFileSettingsCubit extends Cubit { - CreateFileSettingsCubit(super.initialState) { - getInitialSettings(); - } - - Future toggle({bool? value}) async { - await getIt().set( - KVKeys.showRenameDialogWhenCreatingNewFile, - (value ?? !state).toString(), - ); - emit(value ?? !state); - } - - Future getInitialSettings() async { - final settingsOrFailure = await getIt().getWithFormat( - KVKeys.showRenameDialogWhenCreatingNewFile, - (value) => bool.parse(value), - ); - emit(settingsOrFailure ?? false); - } -} 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 deleted file mode 100644 index 76fec2ecfc..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart +++ /dev/null @@ -1,48 +0,0 @@ -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'; - -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'); - } - } - - String formatDate( - DateTime date, - bool includeTime, [ - UserTimeFormatPB? timeFormat, - ]) { - final format = toFormat; - - if (includeTime) { - switch (timeFormat) { - case UserTimeFormatPB.TwentyFourHour: - return format.add_Hm().format(date); - case UserTimeFormatPB.TwelveHour: - return format.add_jm().format(date); - default: - return format.format(date); - } - } - - return format.format(date); - } -} - -final _toFormat = { - UserDateFormatPB.Locally: _localFmt, - UserDateFormatPB.US: _usFmt, - UserDateFormatPB.ISO: _isoFmt, - UserDateFormatPB.Friendly: _friendlyFmt, - UserDateFormatPB.DayMonthYear: _dmyFmt, -}; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart deleted file mode 100644 index 0dfa2807b9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension TimeFormatter on UserTimeFormatPB { - DateFormat get toFormat => _toFormat[this]!; - - String formatTime(DateTime date) => toFormat.format(date); -} - -final _toFormat = { - UserTimeFormatPB.TwentyFourHour: DateFormat.Hm(), - UserTimeFormatPB.TwelveHour: DateFormat.jm(), -}; 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/notifications/notification_settings_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart deleted file mode 100644 index ea6b3b6f01..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/user_settings_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'notification_settings_cubit.freezed.dart'; - -class NotificationSettingsCubit extends Cubit { - NotificationSettingsCubit() : super(NotificationSettingsState.initial()) { - _initialize(); - } - - final Completer _initCompleter = Completer(); - - late final NotificationSettingsPB _notificationSettings; - - Future _initialize() async { - _notificationSettings = - await UserSettingsBackendService().getNotificationSettings(); - - final showNotificationSetting = await getIt() - .getWithFormat(KVKeys.showNotificationIcon, (v) => bool.parse(v)); - - emit( - state.copyWith( - isNotificationsEnabled: _notificationSettings.notificationsEnabled, - isShowNotificationsIconEnabled: showNotificationSetting ?? true, - ), - ); - - _initCompleter.complete(); - } - - Future toggleNotificationsEnabled() async { - await _initCompleter.future; - - _notificationSettings.notificationsEnabled = !state.isNotificationsEnabled; - - emit( - state.copyWith( - isNotificationsEnabled: _notificationSettings.notificationsEnabled, - ), - ); - - await _saveNotificationSettings(); - } - - Future toogleShowNotificationIconEnabled() async { - await _initCompleter.future; - - emit( - state.copyWith( - isShowNotificationsIconEnabled: !state.isShowNotificationsIconEnabled, - ), - ); - } - - Future _saveNotificationSettings() async { - await _initCompleter.future; - - await getIt().set( - KVKeys.showNotificationIcon, - state.isShowNotificationsIconEnabled.toString(), - ); - - final result = await UserSettingsBackendService() - .setNotificationSettings(_notificationSettings); - result.fold( - (r) => null, - (error) => Log.error(error), - ); - } -} - -@freezed -class NotificationSettingsState with _$NotificationSettingsState { - const NotificationSettingsState._(); - - const factory NotificationSettingsState({ - required bool isNotificationsEnabled, - required bool isShowNotificationsIconEnabled, - }) = _NotificationSettingsState; - - factory NotificationSettingsState.initial() => - const NotificationSettingsState( - isNotificationsEnabled: true, - isShowNotificationsIconEnabled: true, - ); -} 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 deleted file mode 100644 index 26975b00ff..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart +++ /dev/null @@ -1,238 +0,0 @@ -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/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:fixnum/fixnum.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; - -part 'settings_plan_bloc.freezed.dart'; - -class SettingsPlanBloc extends Bloc { - SettingsPlanBloc({ - required this.workspaceId, - required Int64 userId, - }) : super(const _Initial()) { - _service = WorkspaceService( - workspaceId: workspaceId, - userId: userId, - ); - _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()); - } - - final snapshots = await Future.wait([ - _service.getWorkspaceUsage(), - UserBackendService.getWorkspaceSubscriptionInfo(workspaceId), - ]); - - FlowyError? error; - - final usageResult = snapshots.first.fold( - (s) => s as WorkspaceUsagePB?, - (f) { - error = f; - return null; - }, - ); - - final subscriptionInfo = snapshots[1].fold( - (s) => s as WorkspaceSubscriptionInfoPB, - (f) { - error = f; - return null; - }, - ); - - if (usageResult == null || - subscriptionInfo == null || - error != null) { - return emit(SettingsPlanState.error(error: error)); - } - - emit( - SettingsPlanState.ready( - workspaceUsage: usageResult, - subscriptionInfo: subscriptionInfo, - successfulPlanUpgrade: withSuccessfulUpgrade, - ), - ); - - if (withSuccessfulUpgrade != null) { - emit( - SettingsPlanState.ready( - workspaceUsage: usageResult, - subscriptionInfo: subscriptionInfo, - ), - ); - } - }, - addSubscription: (plan) async { - 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, - ), - ); - }, - 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, - ), - ); - }, - paymentSuccessful: (plan) { - final readyState = state.mapOrNull(ready: (state) => state); - if (readyState == null) { - return; - } - - add( - SettingsPlanEvent.started( - withSuccessfulUpgrade: plan, - shouldLoad: false, - ), - ); - }, - ); - }); - } - - late final String workspaceId; - late final WorkspaceService _service; - late final IUserBackendService _userService; - late final SubscriptionSuccessListenable _successListenable; - - Future _onPaymentSuccessful() async => add( - SettingsPlanEvent.paymentSuccessful( - plan: _successListenable.subscribedPlan, - ), - ); - - @override - Future close() async { - _successListenable.removeListener(_onPaymentSuccessful); - return super.close(); - } -} - -@freezed -class SettingsPlanEvent with _$SettingsPlanEvent { - const factory SettingsPlanEvent.started({ - @Default(null) SubscriptionPlanPB? withSuccessfulUpgrade, - @Default(true) bool shouldLoad, - }) = _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; -} - -@freezed -class SettingsPlanState with _$SettingsPlanState { - const factory SettingsPlanState.initial() = _Initial; - - const factory SettingsPlanState.loading() = _Loading; - - const factory SettingsPlanState.error({ - @Default(null) FlowyError? error, - }) = _Error; - - const factory SettingsPlanState.ready({ - required WorkspaceUsagePB workspaceUsage, - required WorkspaceSubscriptionInfoPB subscriptionInfo, - @Default(null) SubscriptionPlanPB? successfulPlanUpgrade, - @Default(false) bool downgradeProcessing, - }) = _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 deleted file mode 100644 index 9d91ade4d3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart +++ /dev/null @@ -1,123 +0,0 @@ -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 => - 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', - }; -} - -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 deleted file mode 100644 index ddaca15f5c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_usage_ext.dart +++ /dev/null @@ -1,25 +0,0 @@ -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 currentBlobInGb => - _storageNumberFormat.format(storageBytes.toInt() / 1024 / 1024 / 1024); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/prelude.dart index 926ff91788..3917b54aaf 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/prelude.dart @@ -1,3 +1 @@ -export 'application_data_storage.dart'; -export 'create_file_settings_cubit.dart'; export 'settings_dialog_bloc.dart'; 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 deleted file mode 100644 index 7a1d3efc45..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/setting_file_importer_bloc.dart +++ /dev/null @@ -1,93 +0,0 @@ -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'; -import 'package:appflowy_backend/protobuf/flowy-user/import_data.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'setting_file_importer_bloc.freezed.dart'; - -class SettingFileImportBloc - extends Bloc { - SettingFileImportBloc() : super(SettingFileImportState.initial()) { - on( - (event, emit) async { - await event.when( - 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; - } - - emit( - state.copyWith(loadingState: const LoadingState.loading()), - ); - final result = - await UserEventImportAppFlowyDataFolder(payload).send(); - if (!isClosed) { - add(SettingFileImportEvent.finishImport(result)); - } - }, - finishImport: (result) { - result.fold( - (l) { - emit( - state.copyWith( - successOrFail: FlowyResult.success(null), - loadingState: - LoadingState.finish(FlowyResult.success(null)), - ), - ); - }, - (err) { - Log.error(err); - emit( - state.copyWith( - successOrFail: FlowyResult.failure(err), - loadingState: LoadingState.finish(FlowyResult.failure(err)), - ), - ); - }, - ); - }, - ); - }, - ); - } -} - -@freezed -class SettingFileImportEvent with _$SettingFileImportEvent { - const factory SettingFileImportEvent.importAppFlowyDataFolder(String path) = - _ImportAppFlowyDataFolder; - const factory SettingFileImportEvent.finishImport( - FlowyResult result, - ) = _ImportResult; -} - -@freezed -class SettingFileImportState with _$SettingFileImportState { - const factory SettingFileImportState({ - required LoadingState loadingState, - required FlowyResult? successOrFail, - }) = _SettingFileImportState; - - factory SettingFileImportState.initial() => const SettingFileImportState( - loadingState: LoadingState.idle(), - successOrFail: null, - ); -} 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..4b0f3de9df 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,125 +1,56 @@ 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'; +import 'package:dartz/dartz.dart'; part 'settings_dialog_bloc.freezed.dart'; enum SettingsPage { - // NEW - account, - workspace, - manageData, - shortcuts, - ai, - plan, - billing, - sites, - // OLD - notifications, - cloud, - member, - featureFlags, + appearance, + language, + files, + user, } class SettingsDialogBloc extends Bloc { - SettingsDialogBloc( - this.userProfile, - this.currentWorkspaceMemberRole, { - SettingsPage? initPage, - }) : _userListener = UserListener(userProfile: userProfile), - super(SettingsDialogState.initial(userProfile, initPage)) { - _dispatch(); - } - - final UserProfilePB userProfile; - final AFRolePB? currentWorkspaceMemberRole; final UserListener _userListener; + final UserProfilePB userProfile; + + SettingsDialogBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + super(SettingsDialogState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + setSelectedPage: (SettingsPage page) { + emit(state.copyWith(page: page)); + }, + ); + }); + } @override Future close() async { await _userListener.stop(); - await super.close(); + super.close(); } - void _dispatch() { - on( - (event, emit) async { - 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)); - }, - setSelectedPage: (SettingsPage page) { - emit(state.copyWith(page: page)); - }, - ); - }, - ); - } - - void _profileUpdated( - FlowyResult userProfileOrFailed, - ) { + void _profileUpdated(Either userProfileOrFailed) { userProfileOrFailed.fold( (newUserProfile) => add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)), (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 @@ -136,17 +67,14 @@ class SettingsDialogEvent with _$SettingsDialogEvent { class SettingsDialogState with _$SettingsDialogState { const factory SettingsDialogState({ required UserProfilePB userProfile, + required Either successOrFailure, 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, + successOrFailure: left(unit), + page: SettingsPage.appearance, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart index 9d141db9ac..430a95dd77 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsFileExportState { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart index 696bb9c348..8c8feadcb1 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart @@ -1,10 +1,13 @@ +import 'dart:io'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:path/path.dart' as p; import '../../../startup/tasks/prelude.dart'; @@ -24,7 +27,7 @@ class SettingsLocationCubit extends Cubit { Future resetDataStoragePathToApplicationDefault() async { final directory = await appFlowyApplicationDataDirectory(); - await getIt().setPath(directory.path); + await getIt()._setPath(directory.path); emit(SettingsLocationState.didReceivedPath(directory.path)); } @@ -34,13 +37,83 @@ class SettingsLocationCubit extends Cubit { } Future _init() async { - // The backend might change the real path that storge the data. So it needs - // to get the path from the backend instead of the KeyValueStorage - await UserEventGetUserSetting().send().then((result) { - result.fold( - (l) => emit(SettingsLocationState.didReceivedPath(l.userFolder)), - (r) => Log.error(r), - ); - }); + final path = await getIt().getPath(); + emit(SettingsLocationState.didReceivedPath(path)); + } +} + +const appFlowyDataFolder = "AppFlowyDataDoNotRename"; + +class ApplicationDataStorage { + ApplicationDataStorage(); + String? _cachePath; + + /// Set the custom path to store the data. + /// If the path is not exists, the path will be created. + /// If the path is invalid, the path will be set to the default path. + Future setCustomPath(String path) async { + if (kIsWeb || Platform.isAndroid || Platform.isIOS) { + Log.info('LocalFileStorage is not supported on this platform.'); + return; + } + + if (Platform.isMacOS) { + // remove the prefix `/Volumes/*` + path = path.replaceFirst(RegExp(r'^/Volumes/[^/]+'), ''); + } else if (Platform.isWindows) { + path = path.replaceAll('/', '\\'); + } + + // If the path is not ends with `AppFlowyData`, we will append the + // `AppFlowyData` to the path. If the path is ends with `AppFlowyData`, + // which means the path is the custom path. + if (p.basename(path) != appFlowyDataFolder) { + path = p.join(path, appFlowyDataFolder); + } + + // create the directory if not exists. + final directory = Directory(path); + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + + _setPath(path); + } + + Future _setPath(String path) async { + if (kIsWeb || Platform.isAndroid || Platform.isIOS) { + Log.info('LocalFileStorage is not supported on this platform.'); + return; + } + + await getIt().set(KVKeys.pathLocation, path); + // clear the cache path, and not set the cache path to the new path because the set path may be invalid + _cachePath = null; + } + + Future getPath() async { + if (_cachePath != null) { + return _cachePath!; + } + + final response = await getIt().get(KVKeys.pathLocation); + String path = await response.fold( + (error) async { + // return the default path if the path is not set + final directory = await appFlowyApplicationDataDirectory(); + return directory.path; + }, + (path) => path, + ); + _cachePath = path; + + // if the path is not exists means the path is invalid, so we should clear the kv store + if (!Directory(path).existsSync()) { + await getIt().clear(); + final directory = await appFlowyApplicationDataDirectory(); + path = directory.path; + } + + return path; } } 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..1d0ad4068b 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 @@ -2,22 +2,13 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/share_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:dartz/dartz.dart'; class BackendExportService { - static Future> - exportDatabaseAsCSV( + static Future> exportDatabaseAsCSV( String viewId, ) async { 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..ffb24288f3 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_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; -} +import 'package:appflowy_backend/protobuf/flowy-folder2/import.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart'; +import 'package:dartz/dartz.dart'; 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 await 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 deleted file mode 100644 index 569b4a4ea4..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'settings_shortcuts_cubit.freezed.dart'; - -@freezed -class ShortcutsState with _$ShortcutsState { - const factory ShortcutsState({ - @Default([]) - List commandShortcutEvents, - @Default(ShortcutsStatus.initial) ShortcutsStatus status, - @Default('') String error, - }) = _ShortcutsState; -} - -enum ShortcutsStatus { - initial, - updating, - success, - failure; - - /// Helper getter for when the [ShortcutsStatus] signifies - /// that the shortcuts have not been loaded yet. - /// - bool get isLoading => [initial, updating].contains(this); - - /// Helper getter for when the [ShortcutsStatus] signifies - /// a failure by itself being [ShortcutsStatus.failure] - /// - bool get isFailure => this == ShortcutsStatus.failure; - - /// Helper getter for when the [ShortcutsStatus] signifies - /// a success by itself being [ShortcutsStatus.success] - /// - bool get isSuccess => this == ShortcutsStatus.success; -} - -class ShortcutsCubit extends Cubit { - ShortcutsCubit(this.service) : super(const ShortcutsState()); - - final SettingsShortcutService service; - - Future fetchShortcuts() async { - emit( - state.copyWith( - status: ShortcutsStatus.updating, - error: '', - ), - ); - - try { - final customizeShortcuts = await service.getCustomizeShortcuts(); - await service.updateCommandShortcuts( - commandShortcutEvents, - customizeShortcuts, - ); - - //sort the shortcuts - commandShortcutEvents.sort( - (a, b) => a.key.toLowerCase().compareTo(b.key.toLowerCase()), - ); - - emit( - state.copyWith( - status: ShortcutsStatus.success, - commandShortcutEvents: commandShortcutEvents, - error: '', - ), - ); - } catch (e) { - emit( - state.copyWith( - status: ShortcutsStatus.failure, - error: LocaleKeys.settings_shortcutsPage_couldNotLoadErrorMsg.tr(), - ), - ); - } - } - - Future updateAllShortcuts() async { - emit(state.copyWith(status: ShortcutsStatus.updating, error: '')); - - try { - await service.saveAllShortcuts(state.commandShortcutEvents); - emit(state.copyWith(status: ShortcutsStatus.success, error: '')); - } catch (e) { - emit( - state.copyWith( - status: ShortcutsStatus.failure, - error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(), - ), - ); - } - } - - Future resetToDefault() async { - emit(state.copyWith(status: ShortcutsStatus.updating, error: '')); - - try { - await service.saveAllShortcuts(defaultCommandShortcutEvents); - await fetchShortcuts(); - } catch (e) { - emit( - state.copyWith( - status: ShortcutsStatus.failure, - error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(), - ), - ); - } - } - - /// Checks if the new command is conflicting with other shortcut - /// We also check using the key, whether this command is a codeblock - /// shortcut, if so we only check a conflict with other codeblock shortcut. - CommandShortcutEvent? getConflict( - CommandShortcutEvent currentShortcut, - String command, - ) { - // 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; - } - } - - return null; - } -} - -extension on CommandShortcutEvent { - bool get isCodeBlockCommand => localizedCodeBlockCommands.contains(this); -} 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 deleted file mode 100644 index af95d5af5a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as p; - -import 'shortcuts_model.dart'; - -class SettingsShortcutService { - /// If file is non null then the SettingsShortcutService uses that - /// file to store all the shortcuts, otherwise uses the default - /// Document Directory. - /// Typically we only intend to pass a file during testing. - SettingsShortcutService({ - File? file, - }) { - _initializeService(file); - } - - late final File _file; - final _initCompleter = Completer(); - - /// Takes in commandShortcuts as an input and saves them to the shortcuts.JSON file. - Future saveAllShortcuts( - List commandShortcuts, - ) async { - final shortcuts = EditorShortcuts( - commandShortcuts: commandShortcuts.toCommandShortcutModelList(), - ); - - await _file.writeAsString( - jsonEncode(shortcuts.toJson()), - flush: true, - ); - } - - /// Checks the file for saved shortcuts. If shortcuts do NOT exist then returns - /// an empty list. If shortcuts exist - /// then calls an utility method i.e getShortcutsFromJson which returns the saved shortcuts. - Future> getCustomizeShortcuts() async { - await _initCompleter.future; - final shortcutsInJson = await _file.readAsString(); - - if (shortcutsInJson.isEmpty) { - return []; - } else { - return getShortcutsFromJson(shortcutsInJson); - } - } - - // 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; - } - - Future updateCommandShortcuts( - List commandShortcuts, - List customizeShortcuts, - ) async { - for (final shortcut in customizeShortcuts) { - final shortcutEvent = commandShortcuts.firstWhereOrNull( - (s) => s.key == shortcut.key && s.command != shortcut.command, - ); - shortcutEvent?.updateCommand(command: shortcut.command); - } - } - - Future resetToDefaultShortcuts() async { - await _initCompleter.future; - await saveAllShortcuts(defaultCommandShortcutEvents); - } - - // Accesses the shortcuts.json file within the default AppFlowy Document Directory or creates a new file if it already doesn't exist. - Future _initializeService(File? file) async { - _file = file ?? await _defaultShortcutFile(); - _initCompleter.complete(); - } - - //returns the default file for storing shortcuts - Future _defaultShortcutFile() async { - final path = await getIt().getPath(); - return File( - p.join(path, 'shortcuts', 'shortcuts.json'), - )..createSync(recursive: true); - } -} - -extension on List { - /// Utility method for converting a CommandShortcutEvent List to a - /// CommandShortcutModal List. This is necessary for creating shortcuts - /// object, which is used for saving the shortcuts list. - List toCommandShortcutModelList() => - map((e) => CommandShortcutModel.fromCommandEvent(e)).toList(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/shortcuts_model.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/shortcuts_model.dart deleted file mode 100644 index 93cebf83a4..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/shortcuts_model.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; - -class EditorShortcuts { - factory EditorShortcuts.fromJson(Map json) => - EditorShortcuts( - commandShortcuts: List.from( - json["commandShortcuts"].map((x) => CommandShortcutModel.fromJson(x)), - ), - ); - - EditorShortcuts({required this.commandShortcuts}); - - final List commandShortcuts; - - Map toJson() => { - "commandShortcuts": - List.from(commandShortcuts.map((x) => x.toJson())), - }; -} - -class CommandShortcutModel { - factory CommandShortcutModel.fromCommandEvent( - CommandShortcutEvent commandShortcutEvent, - ) => - CommandShortcutModel( - key: commandShortcutEvent.key, - command: commandShortcutEvent.command, - ); - - factory CommandShortcutModel.fromJson(Map json) => - CommandShortcutModel( - key: json["key"], - command: json["command"] ?? '', - ); - - const CommandShortcutModel({required this.key, required this.command}); - - final String key; - final String command; - - Map toJson() => {"key": key, "command": command}; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CommandShortcutModel && - key == other.key && - command == other.command; - - @override - int get hashCode => key.hashCode ^ command.hashCode; -} 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 deleted file mode 100644 index b8081cb2d5..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ /dev/null @@ -1,152 +0,0 @@ -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/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:collection/collection.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; - -part 'workspace_settings_bloc.freezed.dart'; - -class WorkspaceSettingsBloc - extends Bloc { - WorkspaceSettingsBloc() : super(WorkspaceSettingsState.initial()) { - on( - (event, emit) async { - await event.when( - initial: (userProfile, workspace) async { - _userService = UserBackendService(userId: userProfile.id); - - try { - final currentWorkspace = - await UserBackendService.getCurrentWorkspace().getOrThrow(); - - final workspaces = - await _userService!.getWorkspaces().getOrThrow(); - if (workspaces.isEmpty) { - workspaces.add( - UserWorkspacePB.create() - ..workspaceId = currentWorkspace.id - ..name = currentWorkspace.name - ..createdAtTimestamp = currentWorkspace.createTime, - ); - } - - final currentWorkspaceInList = workspaces.firstWhereOrNull( - (e) => e.workspaceId == currentWorkspace.id, - ) ?? - workspaces.firstOrNull; - - // We emit here because the next event might take longer. - emit(state.copyWith(workspace: currentWorkspaceInList)); - - if (currentWorkspaceInList == null) { - return; - } - - final members = await _getWorkspaceMembers( - currentWorkspaceInList.workspaceId, - ); - - emit( - state.copyWith( - workspace: currentWorkspaceInList, - members: members, - ), - ); - } catch (e) { - Log.error('Failed to get or create current workspace'); - } - }, - updateWorkspaceName: (name) async { - final request = RenameWorkspacePB( - workspaceId: state.workspace?.workspaceId, - newName: name, - ); - final result = await UserEventRenameWorkspace(request).send(); - - state.workspace!.freeze(); - final update = state.workspace!.rebuild((p0) => p0.name = name); - - result.fold( - (_) => emit(state.copyWith(workspace: update)), - (e) => Log.error('Failed to rename workspace: $e'), - ); - }, - updateWorkspaceIcon: (icon) async { - if (state.workspace == null) { - return null; - } - - final request = ChangeWorkspaceIconPB() - ..workspaceId = state.workspace!.workspaceId - ..newIcon = icon; - final result = await UserEventChangeWorkspaceIcon(request).send(); - - result.fold( - (_) { - state.workspace!.freeze(); - final newWorkspace = - state.workspace!.rebuild((p0) => p0.icon = icon); - - return emit(state.copyWith(workspace: newWorkspace)); - }, - (e) => Log.error('Failed to update workspace icon: $e'), - ); - }, - deleteWorkspace: () async => - emit(state.copyWith(deleteWorkspace: true)), - leaveWorkspace: () async => - emit(state.copyWith(leaveWorkspace: true)), - ); - }, - ); - } - - UserBackendService? _userService; - - Future> _getWorkspaceMembers( - String workspaceId, - ) async { - final data = QueryWorkspacePB()..workspaceId = workspaceId; - final result = await UserEventGetWorkspaceMembers(data).send(); - return result.fold( - (s) => s.items, - (e) { - Log.error('Failed to read workspace members: $e'); - return []; - }, - ); - } -} - -@freezed -class WorkspaceSettingsEvent with _$WorkspaceSettingsEvent { - const factory WorkspaceSettingsEvent.initial({ - required UserProfilePB userProfile, - @Default(null) UserWorkspacePB? workspace, - }) = Initial; - - // Workspace itself - const factory WorkspaceSettingsEvent.updateWorkspaceName(String name) = - UpdateWorkspaceName; - const factory WorkspaceSettingsEvent.updateWorkspaceIcon(String icon) = - UpdateWorkspaceIcon; - const factory WorkspaceSettingsEvent.deleteWorkspace() = DeleteWorkspace; - const factory WorkspaceSettingsEvent.leaveWorkspace() = LeaveWorkspace; -} - -@freezed -class WorkspaceSettingsState with _$WorkspaceSettingsState { - const factory WorkspaceSettingsState({ - @Default(null) UserWorkspacePB? workspace, - @Default([]) List members, - @Default(false) bool deleteWorkspace, - @Default(false) bool leaveWorkspace, - }) = _WorkspaceSettingsState; - - factory WorkspaceSettingsState.initial() => const WorkspaceSettingsState(); -} 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 deleted file mode 100644 index 609b9ce0ae..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/startup/startup.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'; - -part 'folder_bloc.freezed.dart'; - -enum FolderSpaceType { - favorite, - private, - public, - unknown; - - ViewSectionPB get toViewSectionPB { - switch (this) { - case FolderSpaceType.private: - return ViewSectionPB.Private; - case FolderSpaceType.public: - return ViewSectionPB.Public; - case FolderSpaceType.favorite: - case FolderSpaceType.unknown: - throw UnimplementedError(); - } - } -} - -class FolderBloc extends Bloc { - FolderBloc({ - required FolderSpaceType type, - }) : super(FolderState.initial(type)) { - on((event, emit) async { - await event.map( - initial: (e) async { - // fetch the expand status - final isExpanded = await _getFolderExpandStatus(); - emit(state.copyWith(isExpanded: isExpanded)); - }, - expandOrUnExpand: (e) async { - final isExpanded = e.isExpanded ?? !state.isExpanded; - await _setFolderExpandStatus(isExpanded); - emit(state.copyWith(isExpanded: isExpanded)); - }, - ); - }); - } - - Future _setFolderExpandStatus(bool isExpanded) async { - final result = await getIt().get(KVKeys.expandedViews); - var map = {}; - if (result != null) { - map = jsonDecode(result); - } - if (isExpanded) { - // set expand status to true if it's not expanded - map[state.type.name] = true; - } else { - // remove the expand status if it's expanded - map.remove(state.type.name); - } - await getIt().set(KVKeys.expandedViews, jsonEncode(map)); - } - - Future _getFolderExpandStatus() async { - return getIt().get(KVKeys.expandedViews).then((result) { - if (result == null) { - return true; - } - final map = jsonDecode(result); - return map[state.type.name] ?? true; - }); - } -} - -@freezed -class FolderEvent with _$FolderEvent { - const factory FolderEvent.initial() = Initial; - const factory FolderEvent.expandOrUnExpand({ - bool? isExpanded, - }) = ExpandOrUnExpand; -} - -@freezed -class FolderState with _$FolderState { - const factory FolderState({ - required FolderSpaceType type, - required bool isExpanded, - }) = _FolderState; - - factory FolderState.initial( - FolderSpaceType type, - ) => - FolderState( - type: type, - isExpanded: true, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/rename_view/rename_view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/rename_view/rename_view_bloc.dart deleted file mode 100644 index de398ddf06..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/rename_view/rename_view_bloc.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'rename_view_bloc.freezed.dart'; - -class RenameViewBloc extends Bloc { - RenameViewBloc(PopoverController controller) - : _controller = controller, - super(RenameViewState(controller: controller)) { - on((event, emit) { - event.when( - open: () => _controller.show(), - ); - }); - } - - final PopoverController _controller; - - @override - Future close() async { - _controller.close(); - await super.close(); - } -} - -@freezed -class RenameViewEvent with _$RenameViewEvent { - const factory RenameViewEvent.open() = _Open; -} - -@freezed -class RenameViewState with _$RenameViewState { - const factory RenameViewState({ - required PopoverController controller, - }) = _RenameViewState; -} 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 deleted file mode 100644 index 6d6ce05051..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ /dev/null @@ -1,811 +0,0 @@ -import 'dart:async'; -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/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/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_result/appflowy_result.dart'; -import 'package:collection/collection.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'; - -enum SpacePermission { - publicToAll, - private, -} - -class SidebarSection { - const SidebarSection({ - required this.publicViews, - required this.privateViews, - }); - - const SidebarSection.empty() - : publicViews = const [], - privateViews = const []; - - final List publicViews; - final List privateViews; - - List get views => publicViews + privateViews; - - SidebarSection copyWith({ - List? publicViews, - List? privateViews, - }) { - return SidebarSection( - publicViews: publicViews ?? this.publicViews, - privateViews: privateViews ?? this.privateViews, - ); - } -} - -/// 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()) { - on( - (event, emit) async { - await event.when( - initial: (openFirstPage) async { - this.openFirstPage = openFirstPage; - - _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); - emit( - state.copyWith( - spaces: spaces, - currentSpace: currentSpace, - isExpanded: isExpanded, - shouldShowUpgradeDialog: shouldShowUpgradeDialog, - isInitialized: true, - ), - ); - - if (shouldShowUpgradeDialog && !integrationMode().isTest) { - if (!isClosed) { - add(const SpaceEvent.migrate()); - } - } - - if (openFirstPage) { - if (currentSpace != null) { - if (!isClosed) { - add(SpaceEvent.open(currentSpace)); - } - } - } - }, - create: ( - name, - icon, - iconColor, - permission, - createNewPageByDefault, - openAfterCreate, - ) async { - final space = await _createSpace( - name: name, - icon: icon, - iconColor: iconColor, - permission: permission, - ); - - Log.info('create space: $space'); - - if (space != null) { - emit( - state.copyWith( - spaces: [...state.spaces, space], - currentSpace: space, - ), - ); - add(SpaceEvent.open(space)); - Log.info('open space: ${space.name}(${space.id})'); - - if (createNewPageByDefault) { - add( - SpaceEvent.createPage( - name: '', - index: 0, - layout: ViewLayoutPB.Document, - openAfterCreate: openAfterCreate, - ), - ); - Log.info('create page: ${space.name}(${space.id})'); - } - } - }, - delete: (space) async { - 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})'); - }, - rename: (space, name) async { - add( - SpaceEvent.update( - space: space, - name: name, - icon: space.spaceIcon, - iconColor: space.spaceIconColor, - permission: space.spacePermission, - ), - ); - }, - changeIcon: (space, icon, iconColor) async { - add( - SpaceEvent.update( - space: space, - icon: icon, - iconColor: iconColor, - ), - ); - }, - update: (space, name, icon, iconColor, permission) async { - space ??= state.currentSpace; - if (space == null) { - Log.error('update space failed, space is null'); - return; - } - - if (name != null) { - await _rename(space, name); - } - - if (icon != null || iconColor != null || permission != null) { - try { - final extra = space.extra; - final current = extra.isNotEmpty == true - ? jsonDecode(extra) - : {}; - final updated = {}; - if (icon != null) { - updated[ViewExtKeys.spaceIconKey] = icon; - } - if (iconColor != null) { - updated[ViewExtKeys.spaceIconColorKey] = iconColor; - } - if (permission != null) { - updated[ViewExtKeys.spacePermissionKey] = permission.index; - } - final merged = mergeMaps(current, updated); - await ViewBackendService.updateView( - 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'); - } - } - - if (permission != null) { - await ViewBackendService.updateViewsVisibility( - [space], - permission == SpacePermission.publicToAll, - ); - } - }, - 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, - ), - ); - - // don't open the page automatically on mobile - if (UniversalPlatform.isDesktop) { - // open the first page by default - if (currentSpace.childViews.isNotEmpty) { - final firstPage = currentSpace.childViews.first; - emit( - state.copyWith( - lastCreatedPage: firstPage, - ), - ); - } else { - emit( - state.copyWith( - lastCreatedPage: ViewPB(), - ), - ); - } - } - }, - expand: (space, isExpanded) async { - await _setSpaceExpandStatus(space, isExpanded); - emit(state.copyWith(isExpanded: isExpanded)); - }, - createPage: (name, layout, index, openAfterCreate) async { - final parentViewId = state.currentSpace?.id; - if (parentViewId == null) { - return; - } - - final result = await ViewBackendService.createView( - name: name, - layoutType: layout, - parentViewId: parentViewId, - index: index, - openAfterCreate: openAfterCreate, - ); - result.fold( - (view) { - emit( - state.copyWith( - lastCreatedPage: openAfterCreate ? view : null, - createPageResult: FlowyResult.success(null), - ), - ); - }, - (error) { - Log.error('Failed to create root view: $error'); - emit( - state.copyWith( - createPageResult: FlowyResult.failure(error), - ), - ); - }, - ); - }, - didReceiveSpaceUpdate: () async { - final (spaces, _, _) = await _getSpaces(); - final currentSpace = await _getLastOpenedSpace(spaces); - - emit( - state.copyWith( - spaces: spaces, - currentSpace: currentSpace, - ), - ); - }, - reset: (userProfile, workspaceId, openFirstPage) async { - if (this.workspaceId == workspaceId) { - return; - } - - _reset(userProfile, workspaceId); - - add( - SpaceEvent.initial( - openFirstPage: openFirstPage, - ), - ); - }, - migrate: () async { - final result = await migrate(); - emit(state.copyWith(shouldShowUpgradeDialog: !result)); - }, - switchToNextSpace: () async { - final spaces = state.spaces; - if (spaces.isEmpty) { - return; - } - - final currentSpace = state.currentSpace; - if (currentSpace == null) { - return; - } - final currentIndex = spaces.indexOf(currentSpace); - final nextIndex = (currentIndex + 1) % spaces.length; - 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; - WorkspaceSectionsListener? _listener; - bool openFirstPage = false; - - @override - Future close() async { - await _listener?.stop(); - _listener = null; - return super.close(); - } - - Future<(List, List, List)> _getSpaces() async { - final sectionViews = await _getSectionViews(); - if (sectionViews == null || sectionViews.views.isEmpty) { - return ([], [], []); - } - - final publicViews = sectionViews.publicViews.unique((e) => e.id); - final privateViews = sectionViews.privateViews.unique((e) => e.id); - - final publicSpaces = publicViews.where((e) => e.isSpace); - final privateSpaces = privateViews.where((e) => e.isSpace); - - return ([...publicSpaces, ...privateSpaces], publicViews, privateViews); - } - - Future _createSpace({ - required String name, - required String icon, - required String iconColor, - required SpacePermission permission, - String? viewId, - }) async { - final section = switch (permission) { - SpacePermission.publicToAll => ViewSectionPB.Public, - 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, - viewId: viewId, - extra: jsonEncode(extra), - ); - return await result.fold((space) async { - Log.info('Space created: $space'); - return space; - }, (error) { - Log.error('Failed to create space: $error'); - return null; - }); - } - - Future _rename(ViewPB space, String name) async { - final result = - await ViewBackendService.updateView(viewId: space.id, name: name); - return result.fold((_) { - space.freeze(); - return space.rebuild((b) => b.name = name); - }, (error) { - Log.error('Failed to rename space: $error'); - return space; - }); - } - - Future _getSectionViews() async { - try { - final publicViews = await _workspaceService.getPublicViews().getOrThrow(); - final privateViews = - await _workspaceService.getPrivateViews().getOrThrow(); - return SidebarSection( - publicViews: publicViews, - privateViews: privateViews, - ); - } catch (e) { - Log.error('Failed to get section views: $e'); - return null; - } - } - - void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService( - workspaceId: workspaceId, - userId: userProfile.id, - ); - - this.userProfile = userProfile; - this.workspaceId = workspaceId; - - _listener = WorkspaceSectionsListener( - user: userProfile, - workspaceId: workspaceId, - )..start( - sectionChanged: (result) async { - if (isClosed) { - return; - } - add(const SpaceEvent.didReceiveSpaceUpdate()); - }, - ); - } - - void _reset(UserProfilePB userProfile, String workspaceId) { - _listener?.stop(); - _listener = null; - - this.userProfile = userProfile; - this.workspaceId = workspaceId; - } - - Future _getLastOpenedSpace(List spaces) async { - if (spaces.isEmpty) { - return null; - } - - final spaceId = - await getIt().get(KVKeys.lastOpenedSpaceId); - if (spaceId == null) { - return spaces.first; - } - - final space = - spaces.firstWhereOrNull((e) => e.id == spaceId) ?? spaces.first; - return space; - } - - Future _openSpace(ViewPB space) async { - await getIt().set(KVKeys.lastOpenedSpaceId, space.id); - } - - Future _setSpaceExpandStatus(ViewPB? space, bool isExpanded) async { - if (space == null) { - return; - } - - final result = await getIt().get(KVKeys.expandedViews); - var map = {}; - if (result != null) { - map = jsonDecode(result); - } - if (isExpanded) { - // set expand status to true if it's not expanded - map[space.id] = true; - } else { - // remove the expand status if it's expanded - map.remove(space.id); - } - await getIt().set(KVKeys.expandedViews, jsonEncode(map)); - } - - Future _getSpaceExpandStatus(ViewPB? space) async { - if (space == null) { - return true; - } - - return getIt().get(KVKeys.expandedViews).then((result) { - if (result == null) { - return true; - } - final map = jsonDecode(result); - return map[space.id] ?? true; - }); - } - - Future migrate({bool auto = true}) async { - try { - final user = - await UserBackendService.getCurrentUserProfile().getOrThrow(); - final service = UserBackendService(userId: user.id); - final members = - await service.getWorkspaceMembers(workspaceId).getOrThrow(); - final isOwner = members.items - .any((e) => e.role == AFRolePB.Owner && e.email == user.email); - - if (members.items.isEmpty) { - return true; - } - - // only one member in the workspace, migrate it immediately - // only the owner can migrate the public space - if (members.items.length == 1 || isOwner) { - // create a new public space and a new private space - // move all the views in the workspace to the new public/private space - var publicViews = await _workspaceService.getPublicViews().getOrThrow(); - final containsPublicSpace = publicViews.any( - (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 publicSpace = await _createSpace( - name: 'Shared', - icon: builtInSpaceIcons.first, - iconColor: builtInSpaceColors.first, - permission: SpacePermission.publicToAll, - viewId: viewId, - ); - - Log.info('migrating: created a new public space: ${publicSpace?.id}'); - - if (publicSpace != null) { - for (final view in publicViews.reversed) { - if (view.isSpace) { - continue; - } - await ViewBackendService.moveViewV2( - viewId: view.id, - newParentId: publicSpace.id, - prevViewId: null, - ); - Log.info( - 'migrating: migrate ${view.name}(${view.id}) to public space(${publicSpace.id})', - ); - } - } - } - - // create a new private space - final viewId = fixedUuid(user.id.toInt(), UuidType.privateSpace); - var privateViews = await _workspaceService.getPrivateViews().getOrThrow(); - // if there is already a private space, don't migrate the private space - final containsPrivateSpace = privateViews.any( - (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; - } - // only migrate the private space if there are any private views - final privateSpace = await _createSpace( - name: 'Private', - icon: builtInSpaceIcons.last, - iconColor: builtInSpaceColors.last, - permission: SpacePermission.private, - viewId: viewId, - ); - Log.info('migrating: created a new private space: ${privateSpace?.id}'); - - if (privateSpace != null) { - for (final view in privateViews.reversed) { - if (view.isSpace) { - continue; - } - await ViewBackendService.moveViewV2( - viewId: view.id, - newParentId: privateSpace.id, - prevViewId: null, - ); - Log.info( - 'migrating: migrate ${view.name}(${view.id}) to private space(${privateSpace.id})', - ); - } - } - - return true; - } catch (e) { - Log.error('migrate space error: $e'); - return false; - } - } - - Future shouldShowUpgradeDialog({ - required List spaces, - required List publicViews, - required List privateViews, - }) async { - final publicSpaces = spaces.where( - (e) => e.spacePermission == SpacePermission.publicToAll, - ); - if (publicSpaces.isEmpty && publicViews.isNotEmpty) { - return true; - } - - final privateSpaces = spaces.where( - (e) => e.spacePermission == SpacePermission.private, - ); - if (privateSpaces.isEmpty && privateViews.isNotEmpty) { - return true; - } - - 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({ - required bool openFirstPage, - }) = _Initial; - const factory SpaceEvent.create({ - required String name, - required String icon, - 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.update({ - ViewPB? space, - String? name, - String? icon, - String? iconColor, - SpacePermission? permission, - }) = _Update; - const factory SpaceEvent.open(ViewPB space) = _Open; - 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; -} - -@freezed -class SpaceState with _$SpaceState { - const factory SpaceState({ - // use root view with space attributes to represent the space - @Default([]) List spaces, - @Default(null) ViewPB? currentSpace, - @Default(true) bool isExpanded, - @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 deleted file mode 100644 index 0cf436630f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/subscription_success_listenable/subscription_success_listenable.dart +++ /dev/null @@ -1,25 +0,0 @@ -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(); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart deleted file mode 100644 index f27539cddd..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart +++ /dev/null @@ -1,435 +0,0 @@ -import 'dart:convert'; - -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/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'; - -class TabsBloc extends Bloc { - TabsBloc() : super(TabsState()) { - menuSharedState = getIt(); - _dispatch(); - } - - late final MenuSharedState menuSharedState; - - @override - Future close() { - state.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - event.when( - selectTab: (int index) { - if (index != state.currentIndex && - index >= 0 && - index < state.pages) { - emit(state.copyWith(currentIndex: 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)); - _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)); - } - }, - ); - }, - ); - } - - void _setLatestOpenView([ViewPB? view]) { - if (view != null) { - menuSharedState.latestOpenView = view; - } else { - final pageManager = state.currentPageManager; - final notifier = pageManager.plugin.notifier; - if (notifier is ViewPluginNotifier && - menuSharedState.latestOpenView?.id != notifier.view.id) { - 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)); - - /// Adds a [TabsEvent.openPlugin] event for the provided [ViewPB] - void openPlugin( - ViewPB view, { - Map arguments = const {}, - }) { - add( - TabsEvent.openPlugin( - plugin: view.plugin(arguments: arguments), - 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(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart index b9bbb0ff08..f698497db9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart @@ -1,2 +1 @@ export 'settings_user_bloc.dart'; -export 'user_workspace_bloc.dart'; 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..1c1e8b4689 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 @@ -3,131 +3,86 @@ 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-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'; +import 'package:dartz/dartz.dart'; part 'settings_user_bloc.freezed.dart'; class SettingsUserViewBloc extends Bloc { - SettingsUserViewBloc(this.userProfile) - : _userListener = UserListener(userProfile: userProfile), - _userService = UserBackendService(userId: userProfile.id), - super(SettingsUserState.initial(userProfile)) { - _dispatch(); - } - final UserBackendService _userService; final UserListener _userListener; final UserProfilePB userProfile; - @override - Future close() async { - await _userListener.stop(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - _loadUserProfile(); - _userListener.start(onProfileUpdated: _profileUpdated); - }, - didReceiveUserProfile: (UserProfilePB newUserProfile) { - emit(state.copyWith(userProfile: newUserProfile)); - }, - updateUserName: (String name) { - _userService.updateUserProfile(name: name).then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - updateUserIcon: (String iconUrl) { - _userService.updateUserProfile(iconUrl: iconUrl).then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - updateUserEmail: (String email) { - _userService.updateUserProfile(email: email).then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - updateUserPassword: (String oldPassword, String newPassword) { - _userService - .updateUserProfile(password: newPassword) - .then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - removeUserIcon: () { - // Empty Icon URL = No icon - _userService.updateUserProfile(iconUrl: "").then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - ); - }, - ); - } - - void _loadUserProfile() { - UserBackendService.getCurrentUserProfile().then((result) { - if (isClosed) { - return; - } - - result.fold( - (userProfile) => add( - SettingsUserEvent.didReceiveUserProfile(userProfile), - ), - (err) => Log.error(err), + SettingsUserViewBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + _userService = UserBackendService(userId: userProfile.id), + super(SettingsUserState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + await _initUser(); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + updateUserName: (String name) { + _userService.updateUserProfile(name: name).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + updateUserIcon: (String iconUrl) { + _userService.updateUserProfile(iconUrl: iconUrl).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + updateUserOpenAIKey: (openAIKey) { + _userService.updateUserProfile(openAIKey: openAIKey).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, ); }); } - void _profileUpdated( - FlowyResult userProfileOrFailed, - ) => - userProfileOrFailed.fold( - (newUserProfile) => - add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), - (err) => Log.error(err), - ); + @override + Future close() async { + await _userListener.stop(); + super.close(); + } + + Future _initUser() async { + final result = await _userService.initUser(); + result.fold((l) => null, (error) => Log.error(error)); + } + + void _profileUpdated(Either userProfileOrFailed) { + userProfileOrFailed.fold( + (newUserProfile) => + add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + } } @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.removeUserIcon() = _RemoveUserIcon; + const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; + const factory SettingsUserEvent.updateUserIcon(String iconUrl) = + _UpdateUserIcon; + const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) = + _UpdateUserOpenaiKey; const factory SettingsUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, ) = _DidReceiveUserProfile; @@ -137,12 +92,12 @@ class SettingsUserEvent with _$SettingsUserEvent { class SettingsUserState with _$SettingsUserState { const factory SettingsUserState({ required UserProfilePB userProfile, - required FlowyResult successOrFailure, + required Either successOrFailure, }) = _SettingsUserState; factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( userProfile: userProfile, - successOrFailure: FlowyResult.success(null), + successOrFailure: left(unit), ); } 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 deleted file mode 100644 index d14f258462..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ /dev/null @@ -1,562 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/user/application/user_listener.dart'; -import 'package:appflowy/user/application/user_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-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: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'; - -part 'user_workspace_bloc.freezed.dart'; - -class UserWorkspaceBloc extends Bloc { - UserWorkspaceBloc({ - required this.userProfile, - }) : _userService = UserBackendService(userId: userProfile.id), - _listener = UserListener(userProfile: userProfile), - super(UserWorkspaceState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - _listener.start( - onUserWorkspaceListUpdated: (workspaces) => - add(UserWorkspaceEvent.updateWorkspaces(workspaces)), - onUserWorkspaceUpdated: (workspace) { - // If currentWorkspace is updated, eg. Icon or Name, we should notify - // the UI to render the updated information. - final currentWorkspace = state.currentWorkspace; - if (currentWorkspace?.workspaceId == workspace.workspaceId) { - add(UserWorkspaceEvent.updateCurrentWorkspace(workspace)); - } - }, - ); - - final result = await _fetchWorkspaces(); - final currentWorkspace = result.$1; - final workspaces = result.$2; - final isCollabWorkspaceOn = - userProfile.userAuthType == AuthTypePB.Server && - FeatureFlag.collaborativeWorkspace.isOn; - Log.info( - 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' - 'workspaces: ${workspaces.map((e) => e.workspaceId)}, isCollabWorkspaceOn: $isCollabWorkspaceOn', - ); - if (currentWorkspace != null && result.$3 == true) { - Log.info('init open workspace: ${currentWorkspace.workspaceId}'); - await _userService.openWorkspace( - currentWorkspace.workspaceId, - currentWorkspace.workspaceAuthType, - ); - } - - emit( - state.copyWith( - currentWorkspace: currentWorkspace, - workspaces: workspaces, - isCollabWorkspaceOn: isCollabWorkspaceOn, - actionResult: null, - ), - ); - }, - fetchWorkspaces: () async { - final result = await _fetchWorkspaces(); - - final currentWorkspace = result.$1; - final workspaces = result.$2; - Log.info( - 'fetch workspaces: current workspace: ${currentWorkspace?.workspaceId}, workspaces: ${workspaces.map((e) => e.workspaceId)}', - ); - - emit( - state.copyWith( - 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 { - emit( - state.copyWith( - actionResult: const UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.create, - isLoading: true, - result: null, - ), - ), - ); - final result = await _userService.createUserWorkspace( - name, - authType, - ); - final workspaces = result.fold( - (s) => [...state.workspaces, s], - (e) => state.workspaces, - ); - emit( - state.copyWith( - workspaces: workspaces, - actionResult: UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.create, - isLoading: false, - result: result, - ), - ), - ); - // open the created workspace by default - result - ..onSuccess((s) { - Log.info('create workspace success: $s'); - add( - OpenWorkspace( - s.workspaceId, - s.workspaceAuthType, - ), - ); - }) - ..onFailure((f) { - Log.error('create workspace error: $f'); - }); - }, - deleteWorkspace: (workspaceId) async { - Log.info('try to delete workspace: $workspaceId'); - emit( - state.copyWith( - actionResult: const UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.delete, - isLoading: true, - result: null, - ), - ), - ); - final remoteWorkspaces = await _fetchWorkspaces().then( - (value) => value.$2, - ); - if (state.workspaces.length <= 1 || remoteWorkspaces.length <= 1) { - // do not allow to delete the last workspace, otherwise the user - // cannot do create workspace again - Log.error('cannot delete the only workspace'); - final result = FlowyResult.failure( - FlowyError( - code: ErrorCode.Internal, - msg: LocaleKeys.workspace_cannotDeleteTheOnlyWorkspace.tr(), - ), - ); - return emit( - state.copyWith( - actionResult: UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.delete, - result: result, - isLoading: false, - ), - ), - ); - } - - 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, - ); - 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, - ), - ); - } - }) - ..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( - workspaces: workspaces, - actionResult: UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.delete, - result: result, - isLoading: false, - ), - ), - ); - }, - openWorkspace: (workspaceId, authType) async { - emit( - state.copyWith( - actionResult: const UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.open, - isLoading: true, - result: null, - ), - ), - ); - final result = await _userService.openWorkspace( - workspaceId, - authType, - ); - final currentWorkspace = result.fold( - (s) => state.workspaces.firstWhereOrNull( - (e) => e.workspaceId == workspaceId, - ), - (e) => state.currentWorkspace, - ); - - result - ..onSuccess((s) { - Log.info( - 'open workspace success: $workspaceId, current workspace: ${currentWorkspace?.toProto3Json()}', - ); - }) - ..onFailure((f) { - Log.error('open workspace error: $f'); - }); - - emit( - state.copyWith( - currentWorkspace: currentWorkspace, - actionResult: UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.open, - isLoading: false, - result: result, - ), - ), - ); - }, - renameWorkspace: (workspaceId, name) async { - final result = - await _userService.renameWorkspace(workspaceId, name); - final workspaces = result.fold( - (s) => state.workspaces.map( - (e) { - if (e.workspaceId == workspaceId) { - e.freeze(); - return e.rebuild((p0) { - p0.name = name; - }); - } - return e; - }, - ).toList(), - (f) => state.workspaces, - ); - final currentWorkspace = workspaces.firstWhere( - (e) => e.workspaceId == state.currentWorkspace?.workspaceId, - ); - - Log.info( - 'rename workspace: $workspaceId, name: $name', - ); - - result.onFailure((f) { - Log.error('rename workspace error: $f'); - }); - - emit( - state.copyWith( - workspaces: workspaces, - currentWorkspace: currentWorkspace, - actionResult: UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.rename, - isLoading: false, - result: result, - ), - ), - ); - }, - 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, - ); - final workspaces = result.fold( - (s) => state.workspaces.map( - (e) { - if (e.workspaceId == workspaceId) { - e.freeze(); - return e.rebuild((p0) { - p0.icon = icon; - }); - } - return e; - }, - ).toList(), - (f) => state.workspaces, - ); - final currentWorkspace = workspaces.firstWhere( - (e) => e.workspaceId == state.currentWorkspace?.workspaceId, - ); - - Log.info( - 'update workspace icon: $workspaceId, icon: $icon', - ); - - result.onFailure((f) { - Log.error('update workspace icon error: $f'); - }); - - emit( - state.copyWith( - workspaces: workspaces, - currentWorkspace: currentWorkspace, - actionResult: UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.updateIcon, - isLoading: false, - result: result, - ), - ), - ); - }, - leaveWorkspace: (workspaceId) async { - final result = await _userService.leaveWorkspace(workspaceId); - final workspaces = result.fold( - (s) => state.workspaces - .where((e) => e.workspaceId != workspaceId) - .toList(), - (e) => state.workspaces, - ); - result - ..onSuccess((_) { - 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, - ), - ); - } - }) - ..onFailure((f) { - Log.error('leave workspace error: $f'); - }); - emit( - state.copyWith( - workspaces: workspaces, - actionResult: UserWorkspaceActionResult( - actionType: UserWorkspaceActionType.leave, - isLoading: false, - result: result, - ), - ), - ); - }, - updateWorkspaces: (workspaces) async { - emit( - state.copyWith( - workspaces: workspaces.items - ..sort( - (a, b) => - a.createdAtTimestamp.compareTo(b.createdAtTimestamp), - ), - ), - ); - }, - updateCurrentWorkspace: (workspace) async { - final workspaces = [...state.workspaces]; - final index = workspaces - .indexWhere((e) => e.workspaceId == workspace.workspaceId); - if (index != -1) { - workspaces[index] = workspace; - } - - emit( - state.copyWith( - currentWorkspace: workspace, - workspaces: workspaces - ..sort( - (a, b) => - a.createdAtTimestamp.compareTo(b.createdAtTimestamp), - ), - ), - ); - }, - ); - }, - ); - } - - @override - Future close() { - _listener.stop(); - return super.close(); - } - - final UserProfilePB userProfile; - final UserBackendService _userService; - final UserListener _listener; - - Future< - ( - UserWorkspacePB? currentWorkspace, - List workspaces, - bool shouldOpenWorkspace, - )> _fetchWorkspaces() async { - try { - final currentWorkspace = - await UserBackendService.getCurrentWorkspace().getOrThrow(); - final workspaces = await _userService.getWorkspaces().getOrThrow(); - if (workspaces.isEmpty) { - workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace)); - } - final currentWorkspaceInList = workspaces - .firstWhereOrNull((e) => e.workspaceId == currentWorkspace.id) ?? - workspaces.firstOrNull; - return ( - currentWorkspaceInList, - workspaces - ..sort( - (a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp), - ), - currentWorkspaceInList?.workspaceId != currentWorkspace.id - ); - } catch (e) { - Log.error('fetch workspace error: $e'); - return (null, [], false); - } - } - - UserWorkspacePB convertWorkspacePBToUserWorkspace(WorkspacePB workspace) { - return UserWorkspacePB.create() - ..workspaceId = workspace.id - ..name = workspace.name - ..createdAtTimestamp = workspace.createTime; - } -} - -@freezed -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.deleteWorkspace(String workspaceId) = - DeleteWorkspace; - const factory UserWorkspaceEvent.openWorkspace( - String workspaceId, - AuthTypePB authType, - ) = OpenWorkspace; - const factory UserWorkspaceEvent.renameWorkspace( - String workspaceId, - String name, - ) = _RenameWorkspace; - const factory UserWorkspaceEvent.updateWorkspaceIcon( - String workspaceId, - String icon, - ) = _UpdateWorkspaceIcon; - const factory UserWorkspaceEvent.leaveWorkspace(String workspaceId) = - LeaveWorkspace; - const factory UserWorkspaceEvent.updateWorkspaces( - RepeatedUserWorkspacePB workspaces, - ) = UpdateWorkspaces; - const factory UserWorkspaceEvent.updateCurrentWorkspace( - UserWorkspacePB workspace, - ) = UpdateCurrentWorkspace; -} - -enum UserWorkspaceActionType { - none, - create, - delete, - open, - rename, - updateIcon, - fetchWorkspaces, - leave; -} - -class UserWorkspaceActionResult { - const UserWorkspaceActionResult({ - required this.actionType, - required this.isLoading, - required this.result, - }); - - final UserWorkspaceActionType actionType; - final bool isLoading; - final FlowyResult? result; - - @override - String toString() { - return 'UserWorkspaceActionResult(actionType: $actionType, isLoading: $isLoading, result: $result)'; - } -} - -@freezed -class UserWorkspaceState with _$UserWorkspaceState { - const UserWorkspaceState._(); - - const factory UserWorkspaceState({ - @Default(null) UserWorkspacePB? currentWorkspace, - @Default([]) List workspaces, - @Default(null) UserWorkspaceActionResult? actionResult, - @Default(false) bool isCollabWorkspaceOn, - }) = _UserWorkspaceState; - - factory UserWorkspaceState.initial() => const UserWorkspaceState(); - - @override - int get hashCode => runtimeType.hashCode; - - final DeepCollectionEquality _deepCollectionEquality = - const DeepCollectionEquality(); - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is UserWorkspaceState && - other.currentWorkspace == currentWorkspace && - _deepCollectionEquality.equals(other.workspaces, workspaces) && - identical(other.actionResult, actionResult); - } -} 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 7c2a4d9b64..f4f4e2ea20 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -1,507 +1,96 @@ -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:dartz/dartz.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_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'; part 'view_bloc.freezed.dart'; class ViewBloc extends Bloc { - ViewBloc({ - required this.view, - this.shouldLoadChildViews = true, - this.engagedInExpanding = false, - }) : 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; final ViewBackendService viewBackendSvc; final ViewListener listener; - final FavoriteListener favoriteListener; - final bool shouldLoadChildViews; - final bool engagedInExpanding; - late ViewExpander expander; + final ViewPB view; + + ViewBloc({ + required this.view, + }) : viewBackendSvc = ViewBackendService(), + listener = ViewListener(viewId: view.id), + super(ViewState.init(view)) { + on((event, emit) async { + await event.map( + initial: (e) { + listener.start( + onViewUpdated: (result) { + add(ViewEvent.viewDidUpdate(left(result))); + }, + ); + emit(state); + }, + setIsEditing: (e) { + emit(state.copyWith(isEditing: e.isEditing)); + }, + viewDidUpdate: (e) { + e.result.fold( + (view) => emit( + state.copyWith(view: view, successOrFailure: left(unit)), + ), + (error) => emit( + state.copyWith(successOrFailure: right(error)), + ), + ); + }, + rename: (e) async { + final result = await ViewBackendService.updateView( + viewId: view.id, + name: e.newName, + ); + emit( + result.fold( + (l) => state.copyWith(successOrFailure: left(unit)), + (error) => state.copyWith(successOrFailure: right(error)), + ), + ); + }, + delete: (e) async { + final result = await ViewBackendService.delete(viewId: view.id); + emit( + result.fold( + (l) => state.copyWith(successOrFailure: left(unit)), + (error) => state.copyWith(successOrFailure: right(error)), + ), + ); + }, + duplicate: (e) async { + final result = await ViewBackendService.duplicate(view: view); + emit( + result.fold( + (l) => state.copyWith(successOrFailure: left(unit)), + (error) => state.copyWith(successOrFailure: right(error)), + ), + ); + }, + ); + }); + } @override Future close() async { await listener.stop(); - await favoriteListener.stop(); - if (engagedInExpanding) { - getIt().unregister(view.id, expander); - } return super.close(); } - - void _dispatch() { - on( - (event, emit) async { - await event.map( - initial: (e) async { - listener.start( - onViewUpdated: (result) { - add(ViewEvent.viewDidUpdate(FlowyResult.success(result))); - }, - onViewChildViewsUpdated: (result) async { - final view = await _updateChildViews(result); - if (!isClosed && view != null) { - add(ViewEvent.viewUpdateChildView(view)); - } - }, - ); - favoriteListener.start( - favoritesUpdated: (result, isFavorite) { - result.fold( - (result) { - final current = result.items - .firstWhereOrNull((v) => v.id == state.view.id); - if (current != null) { - add( - ViewEvent.viewDidUpdate( - FlowyResult.success(current), - ), - ); - } - }, - (error) {}, - ); - }, - ); - final isExpanded = await _getViewIsExpanded(view); - emit(state.copyWith(isExpanded: isExpanded, view: view)); - if (shouldLoadChildViews) { - await _loadChildViews(emit); - } - }, - setIsEditing: (e) { - emit(state.copyWith(isEditing: e.isEditing)); - }, - setIsExpanded: (e) async { - if (e.isExpanded && !state.isExpanded) { - await _loadViewsWhenExpanded(emit, true); - } else { - emit(state.copyWith(isExpanded: e.isExpanded)); - } - await _setViewIsExpanded(view, e.isExpanded); - }, - viewDidUpdate: (e) async { - final result = await ViewBackendService.getView(view.id); - final view_ = result.fold((l) => l, (r) => null); - e.result.fold( - (view) async { - // ignore child view changes because it only contains one level - // children data. - if (_isSameViewIgnoreChildren(view, state.view)) { - // do nothing. - } - emit( - state.copyWith( - view: view_ ?? view, - successOrFailure: FlowyResult.success(null), - ), - ); - }, - (error) => emit( - state.copyWith(successOrFailure: FlowyResult.failure(error)), - ), - ); - }, - rename: (e) async { - final result = await ViewBackendService.updateView( - viewId: view.id, - name: e.newName, - ); - emit( - result.fold( - (l) { - final view = state.view; - view.freeze(); - 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), - ); - }, - ), - ); - }, - 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); - - emit( - result.fold( - (l) { - return state.copyWith( - successOrFailure: FlowyResult.success(null), - isDeleted: true, - ); - }, - (error) => state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ), - ); - await getIt().updateRecentViews( - [view.id], - false, - ); - }, - duplicate: (e) async { - final result = await ViewBackendService.duplicate( - view: view, - openAfterDuplicate: true, - syncAfterDuplicate: true, - includeChildren: true, - suffix: ' (${LocaleKeys.menuAppHeader_pageNameSuffix.tr()})', - ); - emit( - result.fold( - (l) => - state.copyWith(successOrFailure: FlowyResult.success(null)), - (error) => state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ), - ); - }, - move: (value) async { - final result = await ViewBackendService.moveViewV2( - viewId: value.from.id, - newParentId: value.newParentId, - prevViewId: value.prevId, - fromSection: value.fromSection, - toSection: value.toSection, - ); - emit( - result.fold( - (l) { - return state.copyWith( - successOrFailure: FlowyResult.success(null), - ); - }, - (error) => state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ), - ); - }, - createView: (e) async { - final result = await ViewBackendService.createView( - parentViewId: view.id, - name: e.name, - layoutType: e.layoutType, - ext: {}, - openAfterCreate: e.openAfterCreated, - section: e.section, - ); - emit( - result.fold( - (view) => state.copyWith( - lastCreatedView: view, - successOrFailure: FlowyResult.success(null), - ), - (error) => state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ), - ); - }, - viewUpdateChildView: (e) async { - emit( - state.copyWith( - view: e.result, - ), - ); - }, - updateViewVisibility: (value) async { - final view = value.view; - await ViewBackendService.updateViewsVisibility( - [view], - value.isPublic, - ); - }, - updateIcon: (value) async { - await ViewBackendService.updateViewIcon( - view: view, - viewIcon: view.icon.toEmojiIconData(), - ); - }, - collapseAllPages: (value) async { - for (final childView in view.childViews) { - await _setViewIsExpanded(childView, false); - } - add(const ViewEvent.setIsExpanded(false)); - }, - unpublish: (value) async { - if (value.sync) { - await _unpublishPage(view); - } else { - unawaited(_unpublishPage(view)); - } - }, - ); - }, - ); - } - - Future _loadViewsWhenExpanded( - Emitter emit, - bool isExpanded, - ) async { - if (!isExpanded) { - emit( - state.copyWith( - view: view, - isExpanded: false, - isLoading: false, - ), - ); - return; - } - - final viewsOrFailed = - await ViewBackendService.getChildViews(viewId: state.view.id); - - viewsOrFailed.fold( - (childViews) { - state.view.freeze(); - final viewWithChildViews = state.view.rebuild((b) { - b.childViews.clear(); - b.childViews.addAll(childViews); - }); - emit( - state.copyWith( - view: viewWithChildViews, - isExpanded: true, - isLoading: false, - ), - ); - }, - (error) => emit( - state.copyWith( - successOrFailure: FlowyResult.failure(error), - isExpanded: true, - isLoading: false, - ), - ), - ); - } - - Future _loadChildViews( - Emitter emit, - ) async { - final viewsOrFailed = - await ViewBackendService.getChildViews(viewId: state.view.id); - - viewsOrFailed.fold( - (childViews) { - state.view.freeze(); - final viewWithChildViews = state.view.rebuild((b) { - b.childViews.clear(); - b.childViews.addAll(childViews); - }); - emit( - state.copyWith( - view: viewWithChildViews, - ), - ); - }, - (error) => emit( - state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ), - ); - } - - Future _setViewIsExpanded(ViewPB view, bool isExpanded) async { - final result = await getIt().get(KVKeys.expandedViews); - final Map map; - if (result != null) { - map = jsonDecode(result); - } else { - map = {}; - } - if (isExpanded) { - map[view.id] = true; - } else { - map.remove(view.id); - } - await getIt().set(KVKeys.expandedViews, jsonEncode(map)); - } - - Future _getViewIsExpanded(ViewPB view) { - return getIt().get(KVKeys.expandedViews).then((result) { - if (result == null) { - return false; - } - final map = jsonDecode(result); - return map[view.id] ?? false; - }); - } - - Future _updateChildViews( - ChildViewUpdatePB update, - ) async { - if (update.createChildViews.isNotEmpty) { - // refresh the child views if the update isn't empty - // because there's no info to get the inserted index. - assert(update.parentViewId == this.view.id); - final view = await ViewBackendService.getView( - update.parentViewId, - ); - return view.fold((l) => l, (r) => null); - } - - final view = state.view; - view.freeze(); - final childViews = [...view.childViews]; - if (update.deleteChildViews.isNotEmpty) { - childViews.removeWhere((v) => update.deleteChildViews.contains(v.id)); - return view.rebuild((p0) { - p0.childViews.clear(); - p0.childViews.addAll(childViews); - }); - } - - if (update.updateChildViews.isNotEmpty) { - final view = await ViewBackendService.getView(update.parentViewId); - final childViews = view.fold((l) => l.childViews, (r) => []); - bool isSameOrder = true; - if (childViews.length == update.updateChildViews.length) { - for (var i = 0; i < childViews.length; i++) { - if (childViews[i].id != update.updateChildViews[i].id) { - isSameOrder = false; - break; - } - } - } else { - isSameOrder = false; - } - if (!isSameOrder) { - return view.fold((l) => l, (r) => null); - } - } - - 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); - } - - int _hash(ViewPB view) => Object.hash( - view.id, - view.name, - view.createTime, - view.icon, - view.parentViewId, - view.layout, - ); } @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, - String? prevId, - ViewSectionPB? fromSection, - ViewSectionPB? toSection, - ) = Move; - - const factory ViewEvent.createView( - String name, - ViewLayoutPB layoutType, { - /// open the view after created - @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; + const factory ViewEvent.viewDidUpdate(Either result) = + ViewDidUpdate; } @freezed @@ -509,17 +98,12 @@ class ViewState with _$ViewState { const factory ViewState({ required ViewPB view, required bool isEditing, - required bool isExpanded, - required FlowyResult successOrFailure, - @Default(false) bool isDeleted, - @Default(true) bool isLoading, - @Default(null) ViewPB? lastCreatedView, + required Either successOrFailure, }) = _ViewState; factory ViewState.init(ViewPB view) => ViewState( view: view, - isExpanded: false, isEditing: false, - successOrFailure: FlowyResult.success(null), + successOrFailure: left(unit), ); } 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..065b55d8ff 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -1,366 +1,108 @@ -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'; -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/mobile_grid_page.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database_view/tar_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_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; -class PluginArgumentKeys { - static String selection = "selection"; - static String rowId = "row_id"; - static String blockId = "block_id"; +enum FlowyPlugin { + editor, + kanban, } -class ViewExtKeys { - // used for customizing the font family. - static String fontKey = 'font'; +extension FlowyPluginExtension on FlowyPlugin { + String displayName() { + switch (this) { + case FlowyPlugin.editor: + return "Doc"; + case FlowyPlugin.kanban: + return "Kanban"; + default: + return ""; + } + } - // used for customizing the font layout. - static String fontLayoutKey = 'font_layout'; - - // used for customizing the line height layout. - static String lineHeightLayoutKey = 'line_height_layout'; - - // cover keys - static String coverKey = 'cover'; - static String coverTypeKey = 'type'; - static String coverValueKey = 'value'; - - // is pinned - static String isPinnedKey = 'is_pinned'; - - // space - static String isSpaceKey = 'is_space'; - static String spaceCreatorKey = 'space_creator'; - static String spaceCreatedAtKey = 'space_created_at'; - static String spaceIconKey = 'space_icon'; - static String spaceIconColorKey = 'space_icon_color'; - 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, - ); + bool enable() { + switch (this) { + case FlowyPlugin.editor: + return true; + case FlowyPlugin.kanban: + return false; + default: + return false; + } + } } extension ViewExtension on ViewPB { - String get nameOrDefault => - name.isEmpty ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() : name; + Widget renderThumbnail({Color? iconColor}) { + const String thumbnail = "file_icon"; - bool get isDocument => pluginType == PluginType.document; - bool get isDatabase => [ - PluginType.grid, - PluginType.board, - PluginType.calendar, - ].contains(pluginType); + const Widget widget = FlowySvg(name: thumbnail); + return widget; + } - 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, - ); + PluginType get pluginType { + switch (layout) { + case ViewLayoutPB.Board: + return PluginType.board; + case ViewLayoutPB.Calendar: + return PluginType.calendar; + case ViewLayoutPB.Document: + return PluginType.editor; + case ViewLayoutPB.Grid: + return PluginType.grid; + } - PluginType get pluginType => switch (layout) { - ViewLayoutPB.Board => PluginType.board, - ViewLayoutPB.Calendar => PluginType.calendar, - ViewLayoutPB.Document => PluginType.document, - ViewLayoutPB.Grid => PluginType.grid, - ViewLayoutPB.Chat => PluginType.chat, - _ => throw UnimplementedError(), - }; + throw UnimplementedError; + } - Plugin plugin({ - Map arguments = const {}, - }) { + Plugin plugin({bool listenOnViewChanged = false}) { switch (layout) { case ViewLayoutPB.Board: case ViewLayoutPB.Calendar: case ViewLayoutPB.Grid: - final String? rowId = arguments[PluginArgumentKeys.rowId]; - return DatabaseTabBarViewPlugin( view: this, pluginType: pluginType, - initialRowId: rowId, ); 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, + listenOnViewChanged: listenOnViewChanged, ); - case ViewLayoutPB.Chat: - return AIChatPagePlugin(view: this); } throw UnimplementedError; } - DatabaseTabBarItemBuilder tabBarItem() => switch (layout) { - ViewLayoutPB.Board => BoardPageTabBarBuilderImpl(), - ViewLayoutPB.Calendar => CalendarPageTabBarBuilderImpl(), - ViewLayoutPB.Grid => DesktopGridTabBarBuilderImpl(), - _ => throw UnimplementedError, - }; - - DatabaseTabBarItemBuilder mobileTabBarItem() => switch (layout) { - ViewLayoutPB.Board => BoardPageTabBarBuilderImpl(), - ViewLayoutPB.Calendar => CalendarPageTabBarBuilderImpl(), - ViewLayoutPB.Grid => MobileGridTabBarBuilderImpl(), - _ => throw UnimplementedError, - }; - - FlowySvgData get iconData => layout.icon; - - bool get isSpace { - try { - if (extra.isEmpty) { - return false; - } - - final ext = jsonDecode(extra); - final isSpace = ext[ViewExtKeys.isSpaceKey] ?? false; - return isSpace; - } catch (e) { - return false; + DatabaseTabBarItemBuilder tarBarItem() { + switch (layout) { + case ViewLayoutPB.Board: + return BoardPageTabBarBuilderImpl(); + case ViewLayoutPB.Calendar: + return CalendarPageTabBarBuilderImpl(); + case ViewLayoutPB.Grid: + return GridPageTabBarBuilderImpl(); + case ViewLayoutPB.Document: + throw UnimplementedError; } + throw UnimplementedError; } - SpacePermission get spacePermission { - try { - final ext = jsonDecode(extra); - final permission = ext[ViewExtKeys.spacePermissionKey] ?? 1; - return SpacePermission.values[permission]; - } catch (e) { - return SpacePermission.private; - } - } - - FlowySvg? buildSpaceIconSvg(BuildContext context, {Size? size}) { - 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; - } - // 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, - ); - } catch (e) { - return null; - } - } - - String? get spaceIcon { - try { - final ext = jsonDecode(extra); - final icon = ext[ViewExtKeys.spaceIconKey]; - return icon; - } catch (e) { - return null; - } - } - - String? get spaceIconColor { - try { - final ext = jsonDecode(extra); - final color = ext[ViewExtKeys.spaceIconColorKey]; - return color; - } catch (e) { - return null; - } - } - - bool get isPinned { - try { - final ext = jsonDecode(extra); - final isPinned = ext[ViewExtKeys.isPinnedKey] ?? false; - return isPinned; - } catch (e) { - return false; - } - } - - PageStyleCover? get cover { - if (layout != ViewLayoutPB.Document) { - return null; - } - - if (extra.isEmpty) { - return null; - } - - try { - final ext = jsonDecode(extra); - final cover = ext[ViewExtKeys.coverKey] ?? {}; - final coverType = cover[ViewExtKeys.coverTypeKey] ?? - PageStyleCoverImageType.none.toString(); - final coverValue = cover[ViewExtKeys.coverValueKey] ?? ''; - return PageStyleCover( - type: PageStyleCoverImageType.fromString(coverType), - value: coverValue, - ); - } catch (e) { - return null; - } - } - - PageStyleLineHeightLayout get lineHeightLayout { - if (layout != ViewLayoutPB.Document) { - return PageStyleLineHeightLayout.normal; - } - try { - final ext = jsonDecode(extra); - final lineHeight = ext[ViewExtKeys.lineHeightLayoutKey]; - return PageStyleLineHeightLayout.fromString(lineHeight); - } catch (e) { - return PageStyleLineHeightLayout.normal; - } - } - - PageStyleFontLayout get fontLayout { - if (layout != ViewLayoutPB.Document) { - return PageStyleFontLayout.normal; - } - try { - final ext = jsonDecode(extra); - final fontLayout = ext[ViewExtKeys.fontLayoutKey]; - return PageStyleFontLayout.fromString(fontLayout); - } catch (e) { - return PageStyleFontLayout.normal; + String get iconName { + switch (layout) { + case ViewLayoutPB.Grid: + return 'editor/grid'; + case ViewLayoutPB.Board: + return 'editor/board'; + case ViewLayoutPB.Calendar: + return 'editor/calendar'; + default: + throw Exception('Unknown layout type'); } } } - -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.Chat => FlowySvgs.chat_ai_page_s, - _ => FlowySvgs.icon_document_s, - }; - - bool get isDocumentView => switch (this) { - ViewLayoutPB.Document => true, - ViewLayoutPB.Chat || - ViewLayoutPB.Grid || - ViewLayoutPB.Board || - ViewLayoutPB.Calendar => - false, - _ => throw Exception('Unknown layout type'), - }; - - bool get isDatabaseView => switch (this) { - ViewLayoutPB.Grid || - ViewLayoutPB.Board || - ViewLayoutPB.Calendar => - true, - 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 { - ViewPB? findView(String id) { - for (final view in this) { - if (view.id == id) { - return view; - } - - if (view.childViews.isNotEmpty) { - final v = view.childViews.findView(id); - if (v != null) { - return v; - } - } - } - - return null; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart index 95505b8216..fdb9bc593e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart @@ -1,27 +1,24 @@ import 'dart:async'; import 'dart:typed_data'; - import 'package:appflowy/core/notification/folder_notification.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; // Delete the view from trash, which means the view was deleted permanently -typedef DeleteViewNotifyValue = FlowyResult; +typedef DeleteViewNotifyValue = Either; // The view get updated typedef UpdateViewNotifiedValue = ViewPB; // Restore the view from trash -typedef RestoreViewNotifiedValue = FlowyResult; +typedef RestoreViewNotifiedValue = Either; // Move the view to trash -typedef MoveToTrashNotifiedValue = FlowyResult; +typedef MoveToTrashNotifiedValue = Either; class ViewListener { - ViewListener({required this.viewId}); - StreamSubscription? _subscription; void Function(UpdateViewNotifiedValue)? _updatedViewNotifier; void Function(ChildViewUpdatePB)? _updateViewChildViewsNotifier; @@ -33,6 +30,10 @@ class ViewListener { FolderNotificationParser? _parser; final String viewId; + ViewListener({ + required this.viewId, + }); + void start({ void Function(UpdateViewNotifiedValue)? onViewUpdated, void Function(ChildViewUpdatePB)? onViewChildViewsUpdated, @@ -64,7 +65,7 @@ class ViewListener { void _handleObservableType( FolderNotification ty, - FlowyResult result, + Either result, ) { switch (ty) { case FolderNotification.DidUpdateView: @@ -87,23 +88,22 @@ class ViewListener { break; case FolderNotification.DidDeleteView: result.fold( - (payload) => _deletedNotifier - ?.call(FlowyResult.success(ViewPB.fromBuffer(payload))), - (error) => _deletedNotifier?.call(FlowyResult.failure(error)), + (payload) => _deletedNotifier?.call(left(ViewPB.fromBuffer(payload))), + (error) => _deletedNotifier?.call(right(error)), ); break; case FolderNotification.DidRestoreView: result.fold( - (payload) => _restoredNotifier - ?.call(FlowyResult.success(ViewPB.fromBuffer(payload))), - (error) => _restoredNotifier?.call(FlowyResult.failure(error)), + (payload) => + _restoredNotifier?.call(left(ViewPB.fromBuffer(payload))), + (error) => _restoredNotifier?.call(right(error)), ); break; case FolderNotification.DidMoveViewToTrash: result.fold( (payload) => _moveToTrashNotifier - ?.call(FlowyResult.success(DeletedViewPB.fromBuffer(payload))), - (error) => _moveToTrashNotifier?.call(FlowyResult.failure(error)), + ?.call(left(DeletedViewPB.fromBuffer(payload))), + (error) => _moveToTrashNotifier?.call(right(error)), ); break; default: 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 709515f1b3..6e8f71eb14 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -1,18 +1,13 @@ 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/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:dartz/dartz.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'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; class ViewBackendService { - static Future> createView({ + static Future> createView({ /// The [layoutType] is the type of the view. required ViewLayoutPB layoutType, @@ -21,6 +16,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 @@ -38,16 +34,11 @@ class ViewBackendService { /// the database id. For example: "database_id": "xxx" /// Map ext = const {}, - - /// The [index] is the index of the view in the parent view. - /// If the index is null, the view will be added to the end of the list. - int? index, - ViewSectionPB? section, - final String? viewId, }) { final payload = CreateViewPayloadPB.create() ..parentViewId = parentViewId ..name = name + ..desc = desc ?? "" ..layout = layoutType ..setAsCurrent = openAfterCreate ..initialData = initialDataBytes ?? []; @@ -56,25 +47,13 @@ class ViewBackendService { payload.meta.addAll(ext); } - if (index != null) { - payload.index = index; - } - - if (section != null) { - payload.section = section; - } - - if (viewId != null) { - payload.viewId = viewId; - } - return FolderEventCreateView(payload).send(); } /// The orphan view is meant to be a view that is not attached to any parent view. By default, this /// view will not be shown in the view list unless it is attached to a parent view that is shown in /// the view list. - static Future> createOrphanView({ + static Future> createOrphanView({ required String viewId, required ViewLayoutPB layoutType, required String name, @@ -87,92 +66,63 @@ class ViewBackendService { final payload = CreateOrphanViewPayloadPB.create() ..viewId = viewId ..name = name + ..desc = desc ?? "" ..layout = layoutType ..initialData = initialDataBytes ?? []; return FolderEventCreateOrphanView(payload).send(); } - static Future> createDatabaseLinkedView({ + static Future> createDatabaseLinkedView({ required String parentViewId, required String databaseId, required ViewLayoutPB layoutType, required String name, }) { - return createView( + return ViewBackendService.createView( layoutType: layoutType, parentViewId: parentViewId, name: name, - ext: {'database_id': databaseId}, + openAfterCreate: false, + ext: { + 'database_id': databaseId, + }, ); } /// Returns a list of views that are the children of the given [viewId]. - static Future, FlowyError>> getChildViews({ + static Future, FlowyError>> getChildViews({ required String viewId, }) { final payload = ViewIdPB.create()..value = viewId; - return FolderEventGetView(payload).send().then((result) { + return FolderEventReadView(payload).send().then((result) { return result.fold( - (view) => FlowyResult.success(view.childViews), - (error) => FlowyResult.failure(error), + (view) => left(view.childViews), + (error) => right(error), ); }); } - static Future> deleteView({ - required String viewId, - }) { + static Future> delete({required String viewId}) { final request = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventDeleteView(request).send(); } - static Future> deleteViews({ - required List viewIds, - }) { - final request = RepeatedViewIdPB.create()..items.addAll(viewIds); + static Future> deleteView({required String viewId}) { + final request = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventDeleteView(request).send(); } - 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(); + static Future> duplicate({required ViewPB view}) { + return FolderEventDuplicateView(view).send(); } - static Future> favorite({ - required String viewId, - }) { - final request = RepeatedViewIdPB.create()..items.add(viewId); - return FolderEventToggleFavorite(request).send(); - } - - static Future> updateView({ + static Future> updateView({ required String viewId, String? name, - bool? isFavorite, - String? extra, + String? iconURL, + String? coverURL, }) { final payload = UpdateViewPayloadPB.create()..viewId = viewId; @@ -180,42 +130,18 @@ class ViewBackendService { payload.name = name; } - if (isFavorite != null) { - payload.isFavorite = isFavorite; + if (iconURL != null) { + payload.iconUrl = iconURL; } - if (extra != null) { - payload.extra = extra; + if (coverURL != null) { + payload.coverUrl = coverURL; } return FolderEventUpdateView(payload).send(); } - static Future> updateViewIcon({ - required ViewPB view, - required EmojiIconData viewIcon, - }) { - final viewId = view.id; - final oldIcon = view.icon.toEmojiIconData(); - final icon = viewIcon.toViewIcon(); - 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(); - } - - // deprecated - static Future> moveView({ + static Future> moveView({ required String viewId, required int fromIndex, required int toIndex, @@ -228,191 +154,59 @@ class ViewBackendService { return FolderEventMoveView(payload).send(); } - /// Move the view to the new parent view. - /// - /// supports nested view - /// if the [prevViewId] is null, the view will be moved to the beginning of the list - static Future> moveViewV2({ - required String viewId, - required String newParentId, - required String? prevViewId, - ViewSectionPB? fromSection, - ViewSectionPB? toSection, - }) { - final payload = MoveNestedViewPayloadPB( - viewId: viewId, - newParentId: newParentId, - prevViewId: prevViewId, - fromSection: fromSection, - toSection: toSection, - ); - - return FolderEventMoveNestedView(payload).send(); - } - - /// Fetches a flattened list of all Views. - /// - /// Views do not contain their children in this list, as they all exist - /// in the same level in this version. - /// - static Future> getAllViews() async { - return FolderEventGetAllViews().send(); - } - - static Future> getView( - String viewId, + Future)>> fetchViews( + ViewLayoutPB layoutType, ) async { - final payload = ViewIdPB.create()..value = viewId; - return FolderEventGetView(payload).send(); + final result = <(ViewPB, List)>[]; + return FolderEventGetCurrentWorkspace().send().then((value) async { + final workspaces = value.getLeftOrNull(); + if (workspaces != null) { + final views = workspaces.workspace.views; + for (final view in views) { + final childViews = await getChildViews(viewId: view.id).then( + (value) => value + .getLeftOrNull>() + ?.where((e) => e.layout == layoutType) + .toList(), + ); + if (childViews != null && childViews.isNotEmpty) { + result.add((view, childViews)); + } + } + } + return result; + }); } - 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, + static Future> getView( + String viewID, ) async { - final payload = ViewIdPB.create()..value = viewId; - return FolderEventGetViewAncestors(payload).send(); + final payload = ViewIdPB.create()..value = viewID; + return FolderEventReadView(payload).send(); } - Future> getChildView({ + Future> getChildView({ required String parentViewId, required String childViewId, }) async { final payload = ViewIdPB.create()..value = parentViewId; - return FolderEventGetView(payload).send().then((result) { + return FolderEventReadView(payload).send().then((result) { return result.fold( - (app) => FlowyResult.success( + (app) => left( app.childViews.firstWhere((e) => e.id == childViewId), ), - (error) => FlowyResult.failure(error), + (error) => right(error), ); }); } +} - static Future> updateViewsVisibility( - List views, - bool isPublic, - ) async { - final payload = UpdateViewVisibilityStatusPayloadPB( - viewIds: views.map((e) => e.id).toList(), - isPublic: isPublic, - ); - 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; +extension AppFlowy on Either { + T? getLeftOrNull() { + if (isLeft()) { + final result = fold((l) => l, (r) => null); + return result; } - - 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(); + return null; } } 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 deleted file mode 100644 index 27540622ba..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/view_info/view_info_bloc.dart +++ /dev/null @@ -1,104 +0,0 @@ -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'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'view_info_bloc.freezed.dart'; - -class ViewInfoBloc extends Bloc { - ViewInfoBloc({required this.view}) : super(ViewInfoState.initial()) { - on((event, emit) { - event.when( - started: () { - emit( - state.copyWith( - createdAt: view.createTime.toDateTime(), - titleCounters: view.name.getCounter(), - ), - ); - }, - unregisterEditorState: () { - _clearWordCountService(); - emit(state.copyWith(documentCounters: null)); - }, - registerEditorState: (editorState) { - _clearWordCountService(); - _wordCountService = WordCountService(editorState: editorState) - ..addListener(_onWordCountChanged) - ..register(); - - emit( - state.copyWith( - documentCounters: _wordCountService!.documentCounters, - ), - ); - }, - wordCountChanged: () { - emit( - state.copyWith( - documentCounters: _wordCountService?.documentCounters, - ), - ); - }, - titleChanged: (s) { - emit( - state.copyWith( - titleCounters: s.getCounter(), - ), - ); - }, - ); - }); - } - - final ViewPB view; - - WordCountService? _wordCountService; - - @override - Future close() async { - _clearWordCountService(); - await super.close(); - } - - void _onWordCountChanged() => add(const ViewInfoEvent.wordCountChanged()); - - void _clearWordCountService() { - _wordCountService - ?..removeListener(_onWordCountChanged) - ..dispose(); - _wordCountService = null; - } -} - -@freezed -class ViewInfoEvent with _$ViewInfoEvent { - const factory ViewInfoEvent.started() = _Started; - - const factory ViewInfoEvent.unregisterEditorState() = _UnregisterEditorState; - - const factory ViewInfoEvent.registerEditorState({ - required EditorState editorState, - }) = _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 deleted file mode 100644 index 1530c96d32..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart +++ /dev/null @@ -1,92 +0,0 @@ -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'; -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()) { - on( - (event, emit) async { - await event.when( - 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)); - } - }, - ); - }, - ); - - 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.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 deleted file mode 100644 index fdb9dc9321..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:appflowy/workspace/application/view/prelude.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 'view_title_bloc.freezed.dart'; - -class ViewTitleBloc extends Bloc { - ViewTitleBloc({ - required this.view, - }) : viewListener = ViewListener(viewId: view.id), - super(ViewTitleState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - emit( - state.copyWith( - name: view.name, - icon: view.icon.toEmojiIconData(), - view: view, - ), - ); - - viewListener.start( - onViewUpdated: (view) { - add( - ViewTitleEvent.updateNameOrIcon( - view.name, - view.icon.toEmojiIconData(), - view, - ), - ); - }, - ); - }, - updateNameOrIcon: (name, icon, view) async { - emit( - state.copyWith( - name: name, - icon: icon, - view: view, - ), - ); - }, - ); - }, - ); - } - - final ViewPB view; - final ViewListener viewListener; - - @override - Future close() { - viewListener.stop(); - return super.close(); - } -} - -@freezed -class ViewTitleEvent with _$ViewTitleEvent { - const factory ViewTitleEvent.initial() = Initial; - - const factory ViewTitleEvent.updateNameOrIcon( - String name, - EmojiIconData icon, - ViewPB? view, - ) = UpdateNameOrIcon; -} - -@freezed -class ViewTitleState with _$ViewTitleState { - const factory ViewTitleState({ - required String name, - required EmojiIconData icon, - @Default(null) ViewPB? view, - }) = _ViewTitleState; - - factory ViewTitleState.initial() => ViewTitleState( - name: '', - icon: EmojiIconData.none(), - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/prelude.dart index 71145b8e50..129462beb0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/prelude.dart @@ -1,3 +1,3 @@ -export 'workspace_bloc.dart'; +export 'welcome_bloc.dart'; export 'workspace_listener.dart'; export 'workspace_service.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/welcome_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/welcome_bloc.dart new file mode 100644 index 0000000000..88c273c45d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/welcome_bloc.dart @@ -0,0 +1,134 @@ +import 'package:appflowy/user/application/user_listener.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; + +part 'welcome_bloc.freezed.dart'; + +class WelcomeBloc extends Bloc { + final UserBackendService userService; + final UserWorkspaceListener userWorkspaceListener; + WelcomeBloc({required this.userService, required this.userWorkspaceListener}) + : super(WelcomeState.initial()) { + on( + (event, emit) async { + await event.map( + initial: (e) async { + userWorkspaceListener.start( + onWorkspacesUpdated: (result) => + add(WelcomeEvent.workspacesReveived(result)), + ); + // + await _fetchWorkspaces(emit); + }, + openWorkspace: (e) async { + await _openWorkspace(e.workspace, emit); + }, + createWorkspace: (e) async { + await _createWorkspace(e.name, e.desc, emit); + }, + workspacesReveived: (e) async { + emit( + e.workspacesOrFail.fold( + (workspaces) => state.copyWith( + workspaces: workspaces, + successOrFailure: left(unit), + ), + (error) => state.copyWith(successOrFailure: right(error)), + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + await userWorkspaceListener.stop(); + super.close(); + } + + Future _fetchWorkspaces(Emitter emit) async { + final workspacesOrFailed = await userService.getWorkspaces(); + emit( + workspacesOrFailed.fold( + (workspaces) => state.copyWith( + workspaces: workspaces, + successOrFailure: left(unit), + ), + (error) { + Log.error(error); + return state.copyWith(successOrFailure: right(error)); + }, + ), + ); + } + + Future _openWorkspace( + WorkspacePB workspace, + Emitter emit, + ) async { + final result = await userService.openWorkspace(workspace.id); + emit( + result.fold( + (workspaces) => state.copyWith(successOrFailure: left(unit)), + (error) { + Log.error(error); + return state.copyWith(successOrFailure: right(error)); + }, + ), + ); + } + + Future _createWorkspace( + String name, + String desc, + Emitter emit, + ) async { + final result = await userService.createWorkspace(name, desc); + emit( + result.fold( + (workspace) { + return state.copyWith(successOrFailure: left(unit)); + }, + (error) { + Log.error(error); + return state.copyWith(successOrFailure: right(error)); + }, + ), + ); + } +} + +@freezed +class WelcomeEvent with _$WelcomeEvent { + const factory WelcomeEvent.initial() = Initial; + // const factory WelcomeEvent.fetchWorkspaces() = FetchWorkspace; + const factory WelcomeEvent.createWorkspace(String name, String desc) = + CreateWorkspace; + const factory WelcomeEvent.openWorkspace(WorkspacePB workspace) = + OpenWorkspace; + const factory WelcomeEvent.workspacesReveived( + Either, FlowyError> workspacesOrFail, + ) = WorkspacesReceived; +} + +@freezed +class WelcomeState with _$WelcomeState { + const factory WelcomeState({ + required bool isLoading, + required List workspaces, + required Either successOrFailure, + }) = _WelcomeState; + + factory WelcomeState.initial() => WelcomeState( + isLoading: false, + workspaces: List.empty(), + successOrFailure: left(unit), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart deleted file mode 100644 index ed06f16c8f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ /dev/null @@ -1,107 +0,0 @@ -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'; - -part 'workspace_bloc.freezed.dart'; - -class WorkspaceBloc extends Bloc { - WorkspaceBloc({required this.userService}) : super(WorkspaceState.initial()) { - _dispatch(); - } - - final UserBackendService userService; - - void _dispatch() { - on( - (event, emit) async { - await event.map( - initial: (e) async { - await _fetchWorkspaces(emit); - }, - createWorkspace: (e) async { - await _createWorkspace(e.name, e.desc, emit); - }, - workspacesReceived: (e) async { - emit( - e.workspacesOrFail.fold( - (workspaces) => state.copyWith( - workspaces: workspaces, - successOrFailure: FlowyResult.success(null), - ), - (error) => state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ), - ); - }, - ); - }, - ); - } - - Future _fetchWorkspaces(Emitter emit) async { - final workspacesOrFailed = await userService.getWorkspaces(); - emit( - workspacesOrFailed.fold( - (workspaces) => state.copyWith( - workspaces: [], - successOrFailure: FlowyResult.success(null), - ), - (error) { - Log.error(error); - return state.copyWith(successOrFailure: FlowyResult.failure(error)); - }, - ), - ); - } - - Future _createWorkspace( - String name, - String desc, - Emitter emit, - ) async { - final result = - await userService.createUserWorkspace(name, AuthTypePB.Server); - emit( - result.fold( - (workspace) { - return state.copyWith(successOrFailure: FlowyResult.success(null)); - }, - (error) { - Log.error(error); - return state.copyWith(successOrFailure: FlowyResult.failure(error)); - }, - ), - ); - } -} - -@freezed -class WorkspaceEvent with _$WorkspaceEvent { - const factory WorkspaceEvent.initial() = Initial; - const factory WorkspaceEvent.createWorkspace(String name, String desc) = - CreateWorkspace; - const factory WorkspaceEvent.workspacesReceived( - FlowyResult, FlowyError> workspacesOrFail, - ) = WorkspacesReceived; -} - -@freezed -class WorkspaceState with _$WorkspaceState { - const factory WorkspaceState({ - required bool isLoading, - required List workspaces, - required FlowyResult successOrFailure, - }) = _WorkspaceState; - - factory WorkspaceState.initial() => WorkspaceState( - isLoading: false, - workspaces: List.empty(), - successOrFailure: FlowyResult.success(null), - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart index fb3beb4dd5..0e58bd86ba 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart @@ -1,38 +1,34 @@ import 'dart:async'; import 'dart:typed_data'; - import 'package:appflowy/core/notification/folder_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.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-folder2/view.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart'; -typedef RootViewsNotifyValue = FlowyResult, FlowyError>; -typedef WorkspaceNotifyValue = FlowyResult; +typedef AppListNotifyValue = Either, FlowyError>; +typedef WorkspaceNotifyValue = Either; -/// The [WorkspaceListener] listens to the changes including the below: -/// -/// - The root views of the workspace. (Not including the views are inside the root views) -/// - The workspace itself. class WorkspaceListener { - WorkspaceListener({required this.user, required this.workspaceId}); - - final UserProfilePB user; - final String workspaceId; - - PublishNotifier? _appsChangedNotifier = - PublishNotifier(); + PublishNotifier? _appsChangedNotifier = PublishNotifier(); PublishNotifier? _workspaceUpdatedNotifier = PublishNotifier(); FolderNotificationListener? _listener; + final UserProfilePB user; + final String workspaceId; + + WorkspaceListener({ + required this.user, + required this.workspaceId, + }); void start({ - void Function(RootViewsNotifyValue)? appsChanged, + void Function(AppListNotifyValue)? appsChanged, void Function(WorkspaceNotifyValue)? onWorkspaceUpdated, }) { if (appsChanged != null) { @@ -51,22 +47,21 @@ class WorkspaceListener { void _handleObservableType( FolderNotification ty, - FlowyResult result, + Either result, ) { switch (ty) { case FolderNotification.DidUpdateWorkspace: result.fold( (payload) => _workspaceUpdatedNotifier?.value = - FlowyResult.success(WorkspacePB.fromBuffer(payload)), - (error) => - _workspaceUpdatedNotifier?.value = FlowyResult.failure(error), + left(WorkspacePB.fromBuffer(payload)), + (error) => _workspaceUpdatedNotifier?.value = right(error), ); break; case FolderNotification.DidUpdateWorkspaceViews: result.fold( (payload) => _appsChangedNotifier?.value = - FlowyResult.success(RepeatedViewPB.fromBuffer(payload).items), - (error) => _appsChangedNotifier?.value = FlowyResult.failure(error), + left(RepeatedViewPB.fromBuffer(payload).items), + (error) => _appsChangedNotifier?.value = right(error), ); break; default: diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart deleted file mode 100644 index 73c2a9045f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/folder_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' - show UserProfilePB; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flowy_infra/notifier.dart'; - -typedef SectionNotifyValue = FlowyResult; - -/// The [WorkspaceSectionsListener] listens to the changes including the below: -/// -/// - The root views inside different section of the workspace. (Not including the views are inside the root views) -/// depends on the section type(s). -class WorkspaceSectionsListener { - WorkspaceSectionsListener({ - required this.user, - required this.workspaceId, - }); - - final UserProfilePB user; - final String workspaceId; - - final _sectionNotifier = PublishNotifier(); - late final FolderNotificationListener _listener; - - void start({ - void Function(SectionNotifyValue)? sectionChanged, - }) { - if (sectionChanged != null) { - _sectionNotifier.addPublishListener(sectionChanged); - } - - _listener = FolderNotificationListener( - objectId: workspaceId, - handler: _handleObservableType, - ); - } - - void _handleObservableType( - FolderNotification ty, - FlowyResult result, - ) { - switch (ty) { - case FolderNotification.DidUpdateSectionViews: - final FlowyResult value = result.fold( - (s) => FlowyResult.success( - SectionViewsPB.fromBuffer(s), - ), - (f) => FlowyResult.failure(f), - ); - _sectionNotifier.value = value; - break; - default: - break; - } - } - - Future stop() async { - _sectionNotifier.dispose(); - - await _listener.stop(); - } -} 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..8b01f4c684 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -1,107 +1,74 @@ import 'dart:async'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:dartz/dartz.dart'; +import 'package:easy_localization/easy_localization.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; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart' + show CreateViewPayloadPB, MoveViewPayloadPB, ViewLayoutPB, ViewPB; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; class WorkspaceService { - WorkspaceService({required this.workspaceId, required this.userId}); - final String workspaceId; - final fixnum.Int64 userId; - - Future> createView({ + WorkspaceService({ + required this.workspaceId, + }); + Future> createApp({ required String name, - required ViewSectionPB viewSection, - int? index, - ViewLayoutPB? layout, - bool? setAsCurrent, - String? viewId, - String? extra, + String? desc, }) { final payload = CreateViewPayloadPB.create() ..parentViewId = workspaceId ..name = name - ..layout = layout ?? ViewLayoutPB.Document - ..section = viewSection; - - if (index != null) { - payload.index = index; - } - - if (setAsCurrent != null) { - payload.setAsCurrent = setAsCurrent; - } - - if (viewId != null) { - payload.viewId = viewId; - } - - if (extra != null) { - payload.extra = extra; - } + ..desc = desc ?? "" + ..layout = ViewLayoutPB.Document; return FolderEventCreateView(payload).send(); } - Future> getWorkspace() { - return FolderEventReadCurrentWorkspace().send(); + Future> getWorkspace() { + final payload = WorkspaceIdPB.create()..value = workspaceId; + return FolderEventReadAllWorkspaces(payload).send().then((result) { + return result.fold( + (workspaces) { + assert(workspaces.items.length == 1); + + if (workspaces.items.isEmpty) { + return right( + FlowyError.create() + ..msg = LocaleKeys.workspace_notFoundError.tr(), + ); + } else { + return left(workspaces.items[0]); + } + }, + (error) => right(error), + ); + }); } - Future, FlowyError>> getPublicViews() { - final payload = GetWorkspaceViewPB.create()..value = workspaceId; + Future, FlowyError>> getViews() { + final payload = WorkspaceIdPB.create()..value = workspaceId; return FolderEventReadWorkspaceViews(payload).send().then((result) { return result.fold( - (views) => FlowyResult.success(views.items), - (error) => FlowyResult.failure(error), + (apps) => left(apps.items), + (error) => right(error), ); }); } - Future, FlowyError>> getPrivateViews() { - final payload = GetWorkspaceViewPB.create()..value = workspaceId; - return FolderEventReadPrivateViews(payload).send().then((result) { - return result.fold( - (views) => FlowyResult.success(views.items), - (error) => FlowyResult.failure(error), - ); - }); - } - - Future> moveView({ - required String viewId, + Future> moveApp({ + required String appId, required int fromIndex, required int toIndex, }) { final payload = MoveViewPayloadPB.create() - ..viewId = viewId + ..viewId = appId ..from = fromIndex ..to = toIndex; 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); - } - - final payload = UserWorkspaceIdPB(workspaceId: workspaceId); - return UserEventGetWorkspaceUsage(payload).send(); - } - - Future> getBillingPortal() { - return UserEventGetBillingPortal().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 deleted file mode 100644 index 648712bd15..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ /dev/null @@ -1,197 +0,0 @@ -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: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:universal_platform/universal_platform.dart'; - -class CommandPalette extends InheritedWidget { - CommandPalette({ - super.key, - required Widget? child, - required this.notifier, - }) : super( - child: _CommandPaletteController(notifier: notifier, child: child), - ); - - final ValueNotifier notifier; - - static CommandPalette of(BuildContext context) { - final CommandPalette? result = - context.dependOnInheritedWidgetOfExactType(); - - assert(result != null, "CommandPalette could not be found"); - - return result!; - } - - void toggle() => notifier.value = !notifier.value; - - @override - bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; -} - -class _ToggleCommandPaletteIntent extends Intent { - const _ToggleCommandPaletteIntent(); -} - -class _CommandPaletteController extends StatefulWidget { - const _CommandPaletteController({ - required this.child, - required this.notifier, - }); - - final Widget? child; - final ValueNotifier notifier; - - @override - State<_CommandPaletteController> createState() => - _CommandPaletteControllerState(); -} - -class _CommandPaletteControllerState extends State<_CommandPaletteController> { - late ValueNotifier _toggleNotifier = widget.notifier; - bool _isOpen = false; - - @override - void initState() { - super.initState(); - _toggleNotifier.addListener(_onToggle); - } - - @override - void dispose() { - _toggleNotifier.removeListener(_onToggle); - 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; - FlowyOverlay.show( - context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: CommandPaletteModal(shortcutBuilder: _buildShortcut), - ), - ).then((_) { - _isOpen = false; - _toggleNotifier.value = false; - }); - } else if (!_toggleNotifier.value && _isOpen) { - FlowyOverlay.pop(context); - _isOpen = false; - } - } - - @override - Widget build(BuildContext context) => - _buildShortcut(widget.child ?? const SizedBox.shrink()); - - Widget _buildShortcut(Widget child) => FocusableActionDetector( - actions: { - _ToggleCommandPaletteIntent: - CallbackAction<_ToggleCommandPaletteIntent>( - onInvoke: (intent) => - _toggleNotifier.value = !_toggleNotifier.value, - ), - }, - shortcuts: { - LogicalKeySet( - UniversalPlatform.isMacOS - ? LogicalKeyboardKey.meta - : LogicalKeyboardKey.control, - LogicalKeyboardKey.keyP, - ): const _ToggleCommandPaletteIntent(), - }, - child: child, - ); -} - -class CommandPaletteModal extends StatelessWidget { - const CommandPaletteModal({super.key, required this.shortcutBuilder}); - - final Widget Function(Widget) shortcutBuilder; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) => FlowyDialog( - alignment: Alignment.topCenter, - insetPadding: const EdgeInsets.only(top: 100), - constraints: const BoxConstraints( - maxHeight: 600, - maxWidth: 800, - minHeight: 600, - ), - expandHeight: false, - child: shortcutBuilder( - // Change mainAxisSize to max so Expanded works correctly. - Column( - children: [ - SearchField(query: state.query, isLoading: state.searching), - if (state.query?.isEmpty ?? true) ...[ - const Divider(height: 0), - Flexible( - child: RecentViewsList( - onSelected: () => FlowyOverlay.pop(context), - ), - ), - ], - if (state.combinedResponseItems.isNotEmpty && - (state.query?.isNotEmpty ?? false)) ...[ - const Divider(height: 0), - Flexible( - child: SearchResultList( - trash: state.trash, - resultItems: state.combinedResponseItems.values.toList(), - resultSummaries: state.resultSummaries, - ), - ), - ] - // 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(), - ), - ], - ], - ), - ), - ), - ); - } -} - -/// 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, - ), - ); - } -} 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 deleted file mode 100644 index 3bc160ee81..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart +++ /dev/null @@ -1,67 +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/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_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 { - const RecentViewsList({super.key, required this.onSelected}); - - final VoidCallback onSelected; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - RecentViewsBloc()..add(const RecentViewsEvent.initial()), - child: BlocBuilder( - 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(); - - return ListView.separated( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: recentViews.length + 1, - itemBuilder: (_, index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: FlowyText( - LocaleKeys.commandPalette_recentHistory.tr(), - ), - ); - } - - final view = recentViews[index - 1]; - final icon = view.icon.value.isNotEmpty - ? EmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: 18.0, - ) - : FlowySvg(view.iconData, size: const Size.square(20)); - - return SearchRecentViewCell( - icon: SizedBox(width: 24, child: icon), - view: view, - onSelected: onSelected, - ); - }, - separatorBuilder: (_, __) => const Divider(height: 0), - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 1586ab0a7e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart +++ /dev/null @@ -1,182 +0,0 @@ -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/command_palette/command_palette_bloc.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/text_field.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SearchField extends StatefulWidget { - const SearchField({super.key, this.query, this.isLoading = false}); - - final String? query; - final bool isLoading; - - @override - State createState() => _SearchFieldState(); -} - -class _SearchFieldState extends State { - late final FocusNode focusNode; - late final TextEditingController controller; - - @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, - ); - }); - } - - KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { - if (node.hasFocus && - event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.arrowDown) { - node.nextFocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - @override - void dispose() { - focusNode.dispose(); - controller.dispose(); - 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, - Expanded( - child: FlowyTextField( - focusNode: focusNode, - controller: controller, - textStyle: textStyle, - decoration: InputDecoration( - constraints: const BoxConstraints(maxHeight: 48), - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - borderRadius: Corners.s8Border, - ), - isDense: false, - hintText: LocaleKeys.commandPalette_placeholder.tr(), - hintStyle: hintStyle, - errorStyle: theme.textTheme.bodySmall! - .copyWith(color: theme.colorScheme.error), - suffix: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildSuffixIcon(context), - const HSpace(8), - ], - ), - counterText: "", - focusedBorder: const OutlineInputBorder( - borderRadius: Corners.s8Border, - borderSide: BorderSide(color: Colors.transparent), - ), - errorBorder: OutlineInputBorder( - borderRadius: Corners.s8Border, - borderSide: BorderSide(color: theme.colorScheme.error), - ), - ), - onChanged: (value) => context - .read() - .add(CommandPaletteEvent.searchChanged(search: value)), - ), - ), - ], - ); - } - - void _clearSearch() { - controller.clear(); - context - .read() - .add(const CommandPaletteEvent.clearSearch()); - } -} 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_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart deleted file mode 100644 index d90888e3e9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ /dev/null @@ -1,278 +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/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_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, - super.key, - }); - - final List trash; - final List resultItems; - final List resultSummaries; - - @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, - ), - ), - ], - ); - } - - 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(); - }, - ); - } -} - -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/af_focus_manager.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/af_focus_manager.dart deleted file mode 100644 index 17d09b6821..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/af_focus_manager.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Simple ChangeNotifier that can be listened to, notifies the -/// application on events that should trigger focus loss. -/// -/// Eg. lose focus in AppFlowyEditor -/// -abstract class ShouldLoseFocus with ChangeNotifier {} - -/// Private implementation to allow the [AFFocusManager] to -/// call [notifyListeners] without being directly invokable. -/// -class _ShouldLoseFocusImpl extends ShouldLoseFocus { - void notify() => notifyListeners(); -} - -class AFFocusManager extends InheritedWidget { - AFFocusManager({super.key, required super.child}); - - final ShouldLoseFocus loseFocusNotifier = _ShouldLoseFocusImpl(); - - void notifyLoseFocus() { - (loseFocusNotifier as _ShouldLoseFocusImpl).notify(); - } - - @override - bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; - - static AFFocusManager of(BuildContext context) { - final AFFocusManager? result = - context.dependOnInheritedWidgetOfExactType(); - - assert(result != null, "AFFocusManager could not be found"); - return result!; - } - - static AFFocusManager? maybeOf(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); - } -} 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 deleted file mode 100644 index 619ee4e229..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ /dev/null @@ -1,310 +0,0 @@ -import 'package:appflowy/plugins/blank/blank.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/memory_leak_detector.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/home/home_bloc.dart'; -import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; -import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; -import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart'; -import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart'; -import 'package:appflowy/workspace/presentation/widgets/float_bubble/question_bubble.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -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'; - -class DesktopHomeScreen extends StatelessWidget { - const DesktopHomeScreen({super.key}); - - static const routeName = '/DesktopHomeScreen'; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: Future.wait([ - FolderEventGetCurrentWorkspaceSetting().send(), - getIt().getUser(), - ]), - builder: (context, snapshots) { - if (!snapshots.hasData) { - return _buildLoading(); - } - - final workspaceLatest = snapshots.data?[0].fold( - (workspaceLatestPB) => workspaceLatestPB as WorkspaceLatestPB, - (error) => null, - ); - - final userProfile = snapshots.data?[1].fold( - (userProfilePB) => userProfilePB as UserProfilePB, - (error) => null, - ); - - // 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) { - 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( - create: (_) => - HomeBloc(workspaceLatest)..add(const HomeEvent.initial()), - ), - BlocProvider( - create: (_) => HomeSettingBloc( - workspaceLatest, - context.read(), - context.widthPx, - )..add(const HomeSettingEvent.initial()), - ), - BlocProvider( - create: (context) => - FavoriteBloc()..add(const FavoriteEvent.initial()), - ), - ], - child: Scaffold( - floatingActionButton: enableMemoryLeakDetect - ? const FloatingActionButton( - onPressed: dumpMemoryLeak, - child: Icon(Icons.memory), - ) - : null, - body: BlocListener( - listenWhen: (p, c) => p.latestView != c.latestView, - listener: (context, state) { - final view = state.latestView; - if (view != null) { - // Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null. - // All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash. - final currentPageManager = - context.read().state.currentPageManager; - - if (currentPageManager.plugin.pluginType == - PluginType.blank) { - getIt().add( - TabsEvent.openPlugin(plugin: view.plugin()), - ); - } - } - }, - child: BlocBuilder( - buildWhen: (previous, current) => previous != current, - builder: (context, state) => BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: userProfile) - ..add(const UserWorkspaceEvent.initial()), - child: HomeHotKeys( - userProfile: userProfile, - child: FlowyContainer( - Theme.of(context).colorScheme.surface, - child: _buildBody( - context, - userProfile, - workspaceLatest, - ), - ), - ), - ), - ), - ), - ), - ), - ); - }, - ); - } - - Widget _buildLoading() => - const Center(child: CircularProgressIndicator.adaptive()); - - Widget _buildBody( - BuildContext context, - UserProfilePB userProfile, - WorkspaceLatestPB workspaceSetting, - ) { - final layout = HomeLayout(context); - final homeStack = HomeStack( - layout: layout, - delegate: DesktopHomeScreenStackAdaptor(context), - userProfile: userProfile, - ); - final sidebar = _buildHomeSidebar( - context, - layout: layout, - userProfile: userProfile, - workspaceSetting: workspaceSetting, - ); - - final homeMenuResizer = - layout.showMenu ? const SidebarResizer() : const SizedBox.shrink(); - final editPanel = _buildEditPanel(context, layout: layout); - - return _layoutWidgets( - layout: layout, - homeStack: homeStack, - sidebar: sidebar, - editPanel: editPanel, - bubble: const QuestionBubble(), - homeMenuResizer: homeMenuResizer, - ); - } - - Widget _buildHomeSidebar( - BuildContext context, { - required HomeLayout layout, - required UserProfilePB userProfile, - required WorkspaceLatestPB workspaceSetting, - }) { - final homeMenu = HomeSideBar( - userProfile: userProfile, - workspaceSetting: workspaceSetting, - ); - return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); - } - - Widget _buildEditPanel( - BuildContext context, { - required HomeLayout layout, - }) { - final homeBloc = context.read(); - return BlocBuilder( - buildWhen: (previous, current) => - previous.panelContext != current.panelContext, - builder: (context, state) { - final panelContext = state.panelContext; - if (panelContext == null) { - return const SizedBox.shrink(); - } - - return FocusTraversalGroup( - child: RepaintBoundary( - child: EditPanel( - panelContext: panelContext, - onEndEdit: () => homeBloc.add( - const HomeSettingEvent.dismissEditPanel(), - ), - ), - ), - ); - }, - ); - } - - Widget _layoutWidgets({ - required HomeLayout layout, - required Widget sidebar, - required Widget homeStack, - required Widget editPanel, - required Widget bubble, - required Widget homeMenuResizer, - }) { - return Stack( - children: [ - homeStack - .constrained(minWidth: 500) - .positioned( - left: layout.homePageLOffset, - right: layout.homePageROffset, - bottom: 0, - top: 0, - animate: true, - ) - .animate(layout.animDuration, Curves.easeOutQuad), - bubble - .positioned(right: 20, bottom: 16, animate: true) - .animate(layout.animDuration, Curves.easeOut), - editPanel - .animatedPanelX( - duration: layout.animDuration.inMilliseconds * 0.001, - closeX: layout.editPanelWidth, - isClosed: !layout.showEditPanel, - curve: Curves.easeOutQuad, - ) - .positioned( - top: 0, - right: 0, - bottom: 0, - width: layout.editPanelWidth, - ), - sidebar - .animatedPanelX( - closeX: -layout.menuWidth, - isClosed: !layout.showMenu, - curve: Curves.easeOutQuad, - duration: layout.animDuration.inMilliseconds * 0.001, - ) - .positioned(left: 0, top: 0, width: layout.menuWidth, bottom: 0), - homeMenuResizer - .positioned(left: layout.menuWidth) - .animate(layout.animDuration, Curves.easeOutQuad), - ], - ); - } -} - -class DesktopHomeScreenStackAdaptor extends HomeStackDelegate { - DesktopHomeScreenStackAdaptor(this.buildContext); - - final BuildContext buildContext; - - @override - void didDeleteStackWidget(ViewPB view, int? index) { - ViewBackendService.getView(view.parentViewId).then( - (result) => result.fold( - (parentView) { - final List views = parentView.childViews; - if (views.isNotEmpty) { - ViewPB lastView = views.last; - if (index != null && index != 0 && views.length > index - 1) { - lastView = views[index - 1]; - } - - return getIt() - .add(TabsEvent.openPlugin(plugin: lastView.plugin())); - } - - getIt() - .add(TabsEvent.openPlugin(plugin: BlankPagePlugin())); - }, - (err) => Log.error(err), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart deleted file mode 100644 index 68cac12dc5..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -class WorkspaceFailedScreen extends StatefulWidget { - const WorkspaceFailedScreen({super.key}); - - @override - State createState() => _WorkspaceFailedScreenState(); -} - -class _WorkspaceFailedScreenState extends State { - String version = ''; - final String os = Platform.operatingSystem; - - @override - void initState() { - super.initState(); - initVersion(); - } - - Future initVersion() async { - final platformInfo = await PackageInfo.fromPlatform(); - setState(() { - version = platformInfo.version; - }); - } - - @override - Widget build(BuildContext context) { - return Material( - child: Scaffold( - body: Center( - child: SizedBox( - width: 400, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(LocaleKeys.workspace_failedToLoad.tr()), - const VSpace(20), - Row( - children: [ - Flexible( - child: RoundedTextButton( - title: - LocaleKeys.workspace_errorActions_reportIssue.tr(), - height: 40, - onPressed: () => afLaunchUrlString( - 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Workspace%20failed%20to%20load&version=$version&os=$os', - ), - ), - ), - const HSpace(20), - Flexible( - child: RoundedTextButton( - title: LocaleKeys.workspace_errorActions_reachOut.tr(), - height: 40, - onPressed: () => - afLaunchUrlString('https://discord.gg/JucBXeU2FE'), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } -} 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..63b397176f 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'; @@ -11,22 +10,32 @@ import 'package:sized_context/sized_context.dart'; import 'home_sizes.dart'; class HomeLayout { - HomeLayout(BuildContext context) { + late double menuWidth; + late bool showMenu; + late bool menuIsDrawer; + late bool showEditPanel; + late double editPanelWidth; + late double homePageLOffset; + late double homePageROffset; + late double menuSpacing; + late Duration animDuration; + + HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint) { final homeSetting = context.read().state; - showEditPanel = homeSetting.panelContext != null; - menuWidth = max( - HomeSizes.minimumSidebarWidth + homeSetting.resizeOffset, - HomeSizes.minimumSidebarWidth, - ); + showEditPanel = homeSetting.panelContext.isSome(); - final screenWidthPx = context.widthPx; - context - .read() - .add(HomeSettingEvent.checkScreenSize(screenWidthPx)); + menuWidth = Sizes.sideBarMed; + if (context.widthPx >= PageBreaks.desktop) { + menuWidth = Sizes.sideBarLg; + } - showMenu = !homeSetting.isMenuCollapsed; - if (showMenu) { + menuWidth += homeSetting.resizeOffset; + + if (homeSetting.isMenuCollapsed) { + showMenu = false; + } else { + showMenu = true; menuIsDrawer = context.widthPx <= PageBreaks.tabletPortrait; } @@ -34,17 +43,8 @@ class HomeLayout { menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0; animDuration = homeSetting.resizeType.duration(); + editPanelWidth = HomeSizes.editPanelWidth; homePageROffset = showEditPanel ? editPanelWidth : 0; } - - late bool showEditPanel; - late double menuWidth; - late bool showMenu; - late bool menuIsDrawer; - late double homePageLOffset; - late double menuSpacing; - late Duration animDuration; - late double editPanelWidth; - late double homePageROffset; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart new file mode 100644 index 0000000000..392cf22b7e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart @@ -0,0 +1,296 @@ +import 'package:appflowy/plugins/blank/blank.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/appearance.dart'; +import 'package:appflowy/workspace/application/home/home_bloc.dart'; +import 'package:appflowy/workspace/application/home/home_service.dart'; +import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; +import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart'; +import 'package:appflowy/workspace/presentation/widgets/float_bubble/question_bubble.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/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/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:styled_widget/styled_widget.dart'; + +import '../widgets/edit_panel/edit_panel.dart'; +import 'home_layout.dart'; +import 'home_stack.dart'; +import 'menu/menu.dart'; + +class HomeScreen extends StatefulWidget { + final UserProfilePB user; + final WorkspaceSettingPB workspaceSetting; + const HomeScreen(this.user, this.workspaceSetting, {Key? key}) + : super(key: key); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + return HomeBloc(widget.user, widget.workspaceSetting) + ..add(const HomeEvent.initial()); + }, + ), + BlocProvider( + create: (context) { + return HomeSettingBloc( + widget.user, + widget.workspaceSetting, + context.read(), + )..add(const HomeSettingEvent.initial()); + }, + ), + ], + child: HomeHotKeys( + child: Scaffold( + body: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.unauthorized != c.unauthorized, + listener: (context, state) { + if (state.unauthorized) { + Log.error( + "Push to login screen when user token was invalid", + ); + } + }, + ), + BlocListener( + listenWhen: (p, c) => p.latestView != c.latestView, + listener: (context, state) { + final view = state.latestView; + if (view != null) { + // Only open the last opened view if the [HomeStackManager] current opened plugin is blank and the last opened view is not null. + // All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash. + if (getIt().plugin.pluginType == + PluginType.blank) { + getIt().setPlugin( + view.plugin(listenOnViewChanged: true), + ); + getIt().latestOpenView = view; + } + } + }, + ), + ], + child: BlocBuilder( + buildWhen: (previous, current) => previous != current, + builder: (context, state) { + return FlowyContainer( + Theme.of(context).colorScheme.surface, + child: _buildBody(context), + ); + }, + ), + ), + ), + ), + ); + } + + Widget _buildBody(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final layout = HomeLayout(context, constraints); + final homeStack = HomeStack( + layout: layout, + delegate: HomeScreenStackAdaptor( + buildContext: context, + ), + ); + final menu = _buildHomeMenu( + layout: layout, + context: context, + ); + final homeMenuResizer = _buildHomeMenuResizer(context: context); + final editPanel = _buildEditPanel( + layout: layout, + context: context, + ); + const bubble = QuestionBubble(); + return _layoutWidgets( + layout: layout, + homeStack: homeStack, + homeMenu: menu, + editPanel: editPanel, + bubble: bubble, + homeMenuResizer: homeMenuResizer, + ); + }, + ); + } + + Widget _buildHomeMenu({ + required HomeLayout layout, + required BuildContext context, + }) { + final workspaceSetting = widget.workspaceSetting; + final homeMenu = HomeMenu( + user: widget.user, + workspaceSetting: workspaceSetting, + ); + + return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); + } + + Widget _buildEditPanel({ + required BuildContext context, + required HomeLayout layout, + }) { + final homeBloc = context.read(); + return BlocBuilder( + buildWhen: (previous, current) => + previous.panelContext != current.panelContext, + builder: (context, state) { + return state.panelContext.fold( + () => const SizedBox(), + (panelContext) => FocusTraversalGroup( + child: RepaintBoundary( + child: EditPanel( + panelContext: panelContext, + onEndEdit: () => + homeBloc.add(const HomeSettingEvent.dismissEditPanel()), + ), + ), + ), + ); + }, + ); + } + + Widget _buildHomeMenuResizer({ + required BuildContext context, + }) { + 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 homeMenu, + required Widget homeStack, + required Widget editPanel, + required Widget bubble, + required Widget homeMenuResizer, + }) { + return Stack( + children: [ + homeStack + .constrained(minWidth: 500) + .positioned( + left: layout.homePageLOffset, + right: layout.homePageROffset, + bottom: 0, + top: 0, + animate: true, + ) + .animate(layout.animDuration, Curves.easeOut), + bubble + .positioned( + right: 20, + bottom: 16, + animate: true, + ) + .animate(layout.animDuration, Curves.easeOut), + editPanel + .animatedPanelX( + duration: layout.animDuration.inMilliseconds * 0.001, + closeX: layout.editPanelWidth, + isClosed: !layout.showEditPanel, + ) + .positioned( + right: 0, + top: 0, + bottom: 0, + width: layout.editPanelWidth, + ), + homeMenu + .animatedPanelX( + closeX: -layout.menuWidth, + isClosed: !layout.showMenu, + ) + .positioned( + left: 0, + top: 0, + width: layout.menuWidth, + bottom: 0, + animate: true, + ) + .animate(layout.animDuration, Curves.easeOut), + homeMenuResizer + .positioned(left: layout.menuWidth - 5) + .animate(layout.animDuration, Curves.easeOut), + ], + ); + } +} + +class HomeScreenStackAdaptor extends HomeStackDelegate { + final BuildContext buildContext; + + HomeScreenStackAdaptor({ + required this.buildContext, + }); + + @override + void didDeleteStackWidget(ViewPB view, int? index) { + final homeService = HomeService(); + homeService.readApp(appId: view.parentViewId).then((result) { + result.fold( + (parentView) { + final List views = parentView.childViews; + if (views.isNotEmpty) { + var lastView = views.last; + if (index != null && index != 0 && views.length > index - 1) { + lastView = views[index - 1]; + } + + getIt().latestOpenView = lastView; + getIt().setPlugin( + lastView.plugin(listenOnViewChanged: true), + ); + } else { + getIt().latestOpenView = null; + getIt().setPlugin(BlankPagePlugin()); + } + }, + (err) => Log.error(err), + ); + }); + } +} 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..bc5b340b92 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart @@ -1,28 +1,10 @@ class HomeSizes { static const double menuAddButtonHeight = 60; - static const double topBarHeight = 44; + static const double topBarHeight = 60; static const double editPanelTopBarHeight = 60; static const double editPanelWidth = 400; - static const double tabBarHeight = 40; - static const double tabBarWidth = 200; - static const double workspaceSectionHeight = 32; - static const double searchSectionHeight = 30; - static const double newPageSectionHeight = 30; - static const double minimumSidebarWidth = 268; } class HomeInsets { - static const double topBarTitleHorizontalPadding = 12; - static const double topBarTitleVerticalPadding = 12; -} - -class HomeSpaceViewSizes { - static const double leftPadding = 16.0; - static const double viewHeight = 30.0; - - // mobile, m represents mobile - static const double mViewHeight = 48.0; - static const double mViewButtonDimension = 34.0; - static const double mHorizontalPadding = 20.0; - static const double mVerticalPadding = 12.0; + static const double topBarTitlePadding = 12; } 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 ae3b92a702..e47eb8820c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -1,33 +1,17 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; - 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'; -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:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flowy_infra_ui/style_widget/extension.dart'; +import 'package:flowy_infra_ui/widget/spacing.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,533 +21,52 @@ abstract class HomeStackDelegate { void didDeleteStackWidget(ViewPB view, int? index); } -class HomeStack extends StatefulWidget { - const HomeStack({ - super.key, - required this.delegate, - required this.layout, - required this.userProfile, - }); - +class HomeStack extends StatelessWidget { final HomeStackDelegate delegate; final HomeLayout layout; - final UserProfilePB userProfile; - - @override - State createState() => _HomeStackState(); -} - -class _HomeStackState extends State { - int selectedIndex = 0; + const HomeStack({ + required this.delegate, + required this.layout, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { - return BlocProvider.value( - value: getIt(), - child: BlocBuilder( - builder: (context, state) => Column( - children: [ - if (UniversalPlatform.isWindows) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - WindowTitleBar( - leftChildren: [_buildToggleMenuButton(context)], - ), - ], - ), - 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); - } + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + getIt().stackTopBar(layout: layout), + Expanded( + child: Container( + color: Theme.of(context).colorScheme.surface, + child: FocusTraversalGroup( + child: getIt().stackWidget( + onDeleted: (view, index) { + delegate.didDeleteStackWidget(view, index); }, ), ), - 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), - ), - ), - ), - ), - ); - } -} - -class PageStack extends StatefulWidget { - const PageStack({ - super.key, - required this.pageManager, - required this.delegate, - required this.userProfile, - }); - - final PageManager pageManager; - final HomeStackDelegate delegate; - final UserProfilePB userProfile; - - @override - State createState() => _PageStackState(); -} - -class _PageStackState extends State - with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - super.build(context); - - return Container( - color: Theme.of(context).colorScheme.surface, - child: FocusTraversalGroup( - child: widget.pageManager.stackWidget( - userProfile: widget.userProfile, - onDeleted: (view, index) { - widget.delegate.didDeleteStackWidget(view, index); - }, - ), - ), - ); - } - - @override - 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, - required this.index, - required this.children, - this.duration = const Duration(milliseconds: 250), - }); - final int index; final List children; final Duration duration; + const FadingIndexedStack({ + Key? key, + required this.index, + required this.children, + this.duration = const Duration( + milliseconds: 250, + ), + }) : super(key: key); + @override FadingIndexedStackState createState() => FadingIndexedStackState(); } @@ -580,10 +83,8 @@ class FadingIndexedStackState extends State { @override void didUpdateWidget(FadingIndexedStack oldWidget) { if (oldWidget.index == widget.index) return; - _targetOpacity = 0; - SchedulerBinding.instance.addPostFrameCallback( - (_) => setState(() => _targetOpacity = 1), - ); + setState(() => _targetOpacity = 0); + Future.delayed(1.milliseconds, () => setState(() => _targetOpacity = 1)); super.didUpdateWidget(oldWidget); } @@ -592,48 +93,38 @@ 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]); - NavigationCallback get action => (id) => throw UnimplementedError(); + NavigationCallback get action => (id) { + getIt().setStackWithId(id); + }; } -class PageNotifier extends ChangeNotifier { - PageNotifier({Plugin? plugin}) - : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank); - +class HomeStackNotifier extends ChangeNotifier { Plugin _plugin; Widget get titleWidget => _plugin.widgetBuilder.leftBarItem; - Widget tabBarWidget( - String pluginId, [ - bool shortForm = false, - ]) => - _plugin.widgetBuilder.tabBarItem(pluginId, shortForm); + HomeStackNotifier({Plugin? plugin}) + : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank); - 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. + set plugin(Plugin newPlugin) { + _plugin.dispose(); - // Set the plugin view as the latest view. - if (setLatest) { - FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); - } + /// Set the plugin view as the latest view. + FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); _plugin = newPlugin; notifyListeners(); @@ -642,75 +133,44 @@ class PageNotifier extends ChangeNotifier { Plugin get plugin => _plugin; } -// PageManager manages the view for one Tab -class PageManager { - PageManager(); +// HomeStack is initialized as singleton to control the page stack. +class HomeStackManager { + final HomeStackNotifier _notifier = HomeStackNotifier(); + HomeStackManager(); - 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) { + _notifier.plugin = newPlugin; } - 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, - child: Selector( + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _notifier), + ], + child: Selector( selector: (context, notifier) => notifier.titleWidget, - builder: (_, __, child) => MoveWindowDetector( - child: HomeTopBar(layout: layout), - ), + builder: (context, widget, child) { + return MoveWindowDetector(child: HomeTopBar(layout: layout)); + }, ), ); } - Widget stackWidget({ - 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(); - } - + Widget stackWidget({required Function(ViewPB, int?) onDeleted}) { + return MultiProvider( + providers: [ChangeNotifierProvider.value(value: _notifier)], + child: Consumer( + builder: (_, HomeStackNotifier notifier, __) { return FadingIndexedStack( index: getIt().indexOf(notifier.plugin.pluginType), children: getIt().supportPluginTypes.map( @@ -718,20 +178,16 @@ class PageManager { if (pluginType == notifier.plugin.pluginType) { final builder = notifier.plugin.widgetBuilder; final pluginWidget = builder.buildWidget( - context: PluginContext( - onDeleted: onDeleted, - userProfile: userProfile, - ), - shrinkWrap: false, + context: PluginContext(onDeleted: onDeleted), ); return Padding( padding: builder.contentPadding, child: pluginWidget, ); + } else { + return const BlankPage(); } - - return const BlankPage(); }, ).toList(), ); @@ -739,291 +195,39 @@ 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 { - const HomeTopBar({super.key, required this.layout}); +class HomeTopBar extends StatelessWidget { + const HomeTopBar({Key? key, required this.layout}) : super(key: key); 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, - ), - height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding, + color: Theme.of(context).colorScheme.onSecondaryContainer, + height: HomeSizes.topBarHeight, child: Padding( padding: const EdgeInsets.symmetric( - horizontal: HomeInsets.topBarTitleHorizontalPadding, - vertical: HomeInsets.topBarTitleVerticalPadding, + horizontal: HomeInsets.topBarTitlePadding, ), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - HSpace(widget.layout.menuSpacing), + HSpace(layout.menuSpacing), const FlowyNavigation(), const HSpace(16), ChangeNotifierProvider.value( - value: Provider.of(context, listen: false), + value: Provider.of(context, listen: false), child: Consumer( - builder: (_, PageNotifier notifier, __) => + builder: (_, HomeStackNotifier notifier, __) => notifier.plugin.widgetBuilder.rightBarItem ?? const SizedBox.shrink(), ), ), ], ), - ), - ); - } - - @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(), - ), - ), - ), - ), - ], - ), - ), + ).bottomBorder(color: Theme.of(context).dividerColor), ); } } - -/// 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..25fabd3693 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart @@ -1,266 +1,30 @@ import 'dart:io'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_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/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flutter/material.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:provider/provider.dart'; -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. -/// -/// Makes registration of a [HotKey] simple and easy to read, and makes -/// sure the [KeyDownHandler], and other handlers, are grouped with the -/// relevant [HotKey]. -/// -class HotKeyItem { - HotKeyItem({ - required this.hotKey, - this.keyDownHandler, - }); - - final HotKey hotKey; - final KeyDownHandler? keyDownHandler; - - void register() => - hotKeyManager.register(hotKey, keyDownHandler: keyDownHandler); -} - -class HomeHotKeys extends StatefulWidget { - const HomeHotKeys({ - super.key, - required this.userProfile, - required this.child, - }); - - final UserProfilePB userProfile; +class HomeHotKeys extends StatelessWidget { final Widget child; + const HomeHotKeys({required this.child, Key? key}) : super(key: key); @override - State createState() => _HomeHotKeysState(); -} - -class _HomeHotKeysState extends State { - final windowSizeManager = WindowSizeManager(); - - late final items = [ - // Collapse sidebar menu (using slash) - HotKeyItem( - hotKey: HotKey( - KeyCode.backslash, - modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => context - .read() - .add(const HomeSettingEvent.collapseMenu()), - ), - - // Collapse sidebar menu (using .) - HotKeyItem( - hotKey: HotKey( - KeyCode.period, - modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => context - .read() - .add(const HomeSettingEvent.collapseMenu()), - ), - - // Toggle theme mode light/dark - HotKeyItem( - hotKey: HotKey( - KeyCode.keyL, - modifiers: [ - Platform.isMacOS ? KeyModifier.meta : KeyModifier.control, - KeyModifier.shift, - ], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => - context.read().toggleThemeMode(), - ), - - // Close current tab - HotKeyItem( - hotKey: HotKey( - KeyCode.keyW, - modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => - context.read().add(const TabsEvent.closeCurrentTab()), - ), - - // Go to previous tab - HotKeyItem( - hotKey: HotKey( - KeyCode.pageUp, - modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => _selectTab(context, -1), - ), - - // Go to next tab - HotKeyItem( - hotKey: HotKey( - KeyCode.pageDown, - modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => _selectTab(context, 1), - ), - - // Rename current view - HotKeyItem( - hotKey: HotKey( - KeyCode.f2, - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => - getIt().add(const RenameViewEvent.open()), - ), - - // 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), - ), - ), - - ...zoomOutKeyCodes.map( - (keycode) => HotKeyItem( - hotKey: HotKey( - keycode, - 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), - ), - ), - - // Switch to the next space - HotKeyItem( - hotKey: HotKey( - KeyCode.keyO, - modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], - scope: HotKeyScope.inapp, - ), - 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), - ]; - - @override - void initState() { - super.initState(); - _registerHotKeys(context); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _registerHotKeys(context); - } - - @override - Widget build(BuildContext context) => widget.child; - - void _registerHotKeys(BuildContext context) { - for (final element in items) { - element.register(); - } - } - - void _selectTab(BuildContext context, int change) { - final bloc = context.read(); - bloc.add(TabsEvent.selectTab(bloc.state.currentIndex + change)); - } - - Future _scaleWithStep(double step) async { - final currentScaleFactor = await windowSizeManager.getScaleFactor(); - final textScale = (currentScaleFactor + step).clamp( - WindowSizeManager.minScaleFactor, - WindowSizeManager.maxScaleFactor, + Widget build(BuildContext context) { + final HotKey hotKey = HotKey( + KeyCode.backslash, + modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], + // Set hotkey scope (default is HotKeyScope.system) + scope: HotKeyScope.inapp, // Set as inapp-wide hotkey. ); - - Log.info('scale the app from $currentScaleFactor to $textScale'); - - await _scale(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); + hotKeyManager.register( + hotKey, + keyDownHandler: (hotKey) { + context + .read() + .add(const HomeSettingEvent.collapseMenu()); + }, + ); + return child; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/create_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/create_button.dart new file mode 100644 index 0000000000..96e00499ee --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/create_button.dart @@ -0,0 +1,52 @@ +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/extension.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +class NewAppButton extends StatelessWidget { + final Function(String)? press; + + const NewAppButton({this.press, Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + final child = FlowyTextButton( + LocaleKeys.newPageText.tr(), + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + fontColor: Theme.of(context).colorScheme.tertiary, + onPressed: () async => await _showCreateAppDialog(context), + heading: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surface, + ), + child: svgWidget("home/new_app"), + ), + padding: EdgeInsets.symmetric(horizontal: Insets.l, vertical: 20), + ); + + return SizedBox( + height: HomeSizes.menuAddButtonHeight, + child: child, + ).topBorder(color: Theme.of(context).dividerColor); + } + + Future _showCreateAppDialog(BuildContext context) async { + return NavigatorTextFieldDialog( + title: LocaleKeys.newPageText.tr(), + value: "", + confirm: (newValue) { + if (newValue.isNotEmpty && press != null) { + press!(newValue); + } + }, + ).show(context); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart new file mode 100644 index 0000000000..7e0055be70 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart @@ -0,0 +1,146 @@ +import 'package:appflowy/plugins/document/document.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_type.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.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/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class AddButton extends StatelessWidget { + final String parentViewId; + final Function( + PluginBuilder, + String? name, + List? initialDataBytes, + bool openAfterCreated, + ) onSelected; + + const AddButton({ + required this.parentViewId, + Key? key, + required this.onSelected, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final List actions = []; + + // Plugins + actions.addAll( + pluginBuilders() + .map( + (pluginBuilder) => + AddButtonActionWrapper(pluginBuilder: pluginBuilder), + ) + .toList(), + ); + + // Import + actions.addAll( + getIt() + .builders + .whereType() + .map( + (pluginBuilder) => + ImportActionWrapper(pluginBuilder: pluginBuilder), + ) + .toList(), + ); + + return PopoverActionList( + direction: PopoverDirection.bottomWithLeftAligned, + actions: actions, + offset: const Offset(0, 8), + buildChild: (controller) { + return SizedBox( + width: 22, + child: InkWell( + onTap: () => controller.show(), + child: FlowyHover( + style: HoverStyle( + hoverColor: AFThemeExtension.of(context).greySelect, + ), + builder: (context, onHover) => const FlowySvg( + name: 'home/add', + ), + ), + ), + ); + }, + onSelected: (action, controller) { + if (action is AddButtonActionWrapper) { + onSelected(action.pluginBuilder, null, null, true); + } + if (action is ImportActionWrapper) { + showImportPanel( + parentViewId, + context, + (type, name, initialDataBytes) { + if (initialDataBytes == null) { + return; + } + switch (type) { + case ImportType.historyDocument: + case ImportType.historyDatabase: + case ImportType.databaseCSV: + case ImportType.databaseRawData: + onSelected( + action.pluginBuilder, + name, + initialDataBytes, + false, + ); + break; + case ImportType.markdownOrText: + onSelected( + action.pluginBuilder, + name, + initialDataBytes, + true, + ); + break; + } + }, + ); + } + controller.close(); + }, + ); + } +} + +class AddButtonActionWrapper extends ActionCell { + final PluginBuilder pluginBuilder; + + AddButtonActionWrapper({required this.pluginBuilder}); + + @override + Widget? leftIcon(Color iconColor) => FlowySvg(name: pluginBuilder.menuIcon); + + @override + String get name => pluginBuilder.menuName; + + PluginType get pluginType => pluginBuilder.pluginType; +} + +class ImportActionWrapper extends ActionCell { + final DocumentPluginBuilder pluginBuilder; + + ImportActionWrapper({ + required this.pluginBuilder, + }); + + @override + Widget? leftIcon(Color iconColor) => const FlowySvg( + name: 'editor/import', + ); + + @override + String get name => LocaleKeys.moreAction_import.tr(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart new file mode 100644 index 0000000000..82ffd93d40 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart @@ -0,0 +1,204 @@ +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flowy_infra/icon_data.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/workspace/application/app/app_bloc.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flowy_infra/image.dart'; + +import '../menu_app.dart'; +import 'add_button.dart'; + +class MenuAppHeader extends StatelessWidget { + final ViewPB parentView; + const MenuAppHeader( + this.parentView, { + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: MenuAppSizes.headerHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _renderExpandedIcon(context), + // HSpace(MenuAppSizes.iconPadding), + _renderTitle(context), + _renderCreateViewButton(context), + ], + ), + ); + } + + Widget _renderExpandedIcon(BuildContext context) { + return SizedBox( + width: MenuAppSizes.headerHeight, + height: MenuAppSizes.headerHeight, + child: InkWell( + onTap: () { + ExpandableController.of( + context, + rebuildOnChange: false, + required: true, + )?.toggle(); + }, + child: ExpandableIcon( + theme: ExpandableThemeData( + expandIcon: FlowyIconData.drop_down_show, + collapseIcon: FlowyIconData.drop_down_hide, + iconColor: Theme.of(context).colorScheme.tertiary, + iconSize: MenuAppSizes.iconSize, + iconPadding: const EdgeInsets.fromLTRB(0, 0, 10, 0), + hasIcon: false, + ), + ), + ), + ); + } + + Widget _renderTitle(BuildContext context) { + return Expanded( + child: BlocListener( + listenWhen: (p, c) => + (p.latestCreatedView == null && c.latestCreatedView != null), + listener: (context, state) { + final expandableController = ExpandableController.of( + context, + rebuildOnChange: false, + required: true, + )!; + if (!expandableController.expanded) { + expandableController.toggle(); + } + }, + child: AppActionList( + onSelected: (action) { + switch (action) { + case AppDisclosureAction.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.menuAppHeader_renameDialog.tr(), + value: context.read().state.view.name, + confirm: (newValue) { + context.read().add(AppEvent.rename(newValue)); + }, + ).show(context); + + break; + case AppDisclosureAction.delete: + context.read().add(const AppEvent.delete()); + break; + } + }, + ), + ), + ); + } + + Widget _renderCreateViewButton(BuildContext context) { + return Tooltip( + message: LocaleKeys.menuAppHeader_addPageTooltip.tr(), + child: AddButton( + parentViewId: parentView.id, + onSelected: (pluginBuilder, name, initialDataBytes, openAfterCreated) { + context.read().add( + AppEvent.createView( + name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + pluginBuilder.layoutType!, + initialDataBytes: initialDataBytes, + openAfterCreated: openAfterCreated, + ), + ); + }, + ).padding(right: MenuAppSizes.headerPadding), + ); + } +} + +enum AppDisclosureAction { + rename, + delete, +} + +extension AppDisclosureExtension on AppDisclosureAction { + String get name { + switch (this) { + case AppDisclosureAction.rename: + return LocaleKeys.disclosureAction_rename.tr(); + case AppDisclosureAction.delete: + return LocaleKeys.disclosureAction_delete.tr(); + } + } + + Widget icon(Color iconColor) { + switch (this) { + case AppDisclosureAction.rename: + return const FlowySvg(name: 'editor/edit'); + case AppDisclosureAction.delete: + return const FlowySvg(name: 'editor/delete'); + } + } +} + +class AppActionList extends StatelessWidget { + final Function(AppDisclosureAction) onSelected; + const AppActionList({ + required this.onSelected, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + actions: AppDisclosureAction.values + .map((action) => DisclosureActionWrapper(action)) + .toList(), + buildChild: (controller) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => ExpandableController.of( + context, + rebuildOnChange: false, + required: true, + )?.toggle(), + onSecondaryTap: () { + controller.show(); + }, + child: BlocSelector( + selector: (state) => state.view, + builder: (context, app) => FlowyText.medium( + app.name, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.tertiary, + ), + ), + ); + }, + onSelected: (action, controller) { + onSelected(action.inner); + controller.close(); + }, + ); + } +} + +class DisclosureActionWrapper extends ActionCell { + final AppDisclosureAction inner; + + DisclosureActionWrapper(this.inner); + @override + Widget? leftIcon(Color iconColor) => inner.icon(iconColor); + + @override + String get name => inner.name; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart new file mode 100644 index 0000000000..68c2ff4ede --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart @@ -0,0 +1,176 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +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/startup/startup.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:appflowy/workspace/application/settings/share/import_service.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_type.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:file_picker/file_picker.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:path/path.dart' as p; + +typedef ImportCallback = void Function( + ImportType type, + String name, + List? document, +); + +Future showImportPanel( + String parentViewId, + BuildContext context, + ImportCallback callback, +) async { + await FlowyOverlay.show( + context: context, + builder: (context) => FlowyDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: FlowyText.semibold( + LocaleKeys.moreAction_import.tr(), + fontSize: 20, + color: Theme.of(context).colorScheme.tertiary, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 20.0, + ), + child: ImportPanel( + parentViewId: parentViewId, + importCallback: callback, + ), + ), + ), + ); +} + +class ImportPanel extends StatelessWidget { + const ImportPanel({ + super.key, + required this.parentViewId, + required this.importCallback, + }); + + final String parentViewId; + final ImportCallback importCallback; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width * 0.7; + final height = width * 0.5; + return 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, + ), + onTap: () async { + await _importFile(parentViewId, e); + if (context.mounted) { + FlowyOverlay.pop(context); + } + }, + ), + ), + ) + .toList(), + ), + ); + } + + Future _importFile(String parentViewId, ImportType importType) async { + final result = await getIt().pickFiles( + type: FileType.custom, + allowMultiple: importType.allowMultiSelect, + allowedExtensions: importType.allowedExtensions, + ); + if (result == null || result.files.isEmpty) { + return; + } + + 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.markdownOrText: + case ImportType.historyDocument: + final bytes = _documentDataFrom(importType, data); + if (bytes != null) { + await ImportBackendService.importData( + bytes, + name, + parentViewId, + ImportTypePB.HistoryDocument, + ); + } + break; + case ImportType.historyDatabase: + await ImportBackendService.importData( + utf8.encode(data), + name, + parentViewId, + ImportTypePB.HistoryDatabase, + ); + break; + 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'); + } + } + } +} + +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(); + default: + assert(false, 'Unsupported Type $importType'); + return null; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart new file mode 100644 index 0000000000..6463019977 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +enum ImportType { + historyDocument, + historyDatabase, + markdownOrText, + databaseCSV, + databaseRawData; + + @override + String toString() { + switch (this) { + case ImportType.historyDocument: + return LocaleKeys.importPanel_documentFromV010.tr(); + case ImportType.historyDatabase: + return LocaleKeys.importPanel_databaseFromV010.tr(); + case ImportType.markdownOrText: + return LocaleKeys.importPanel_textAndMarkdown.tr(); + case ImportType.databaseCSV: + return LocaleKeys.importPanel_csv.tr(); + case ImportType.databaseRawData: + return LocaleKeys.importPanel_database.tr(); + } + } + + WidgetBuilder get icon => (context) { + final String name; + switch (this) { + case ImportType.historyDocument: + name = 'editor/board'; + case ImportType.historyDatabase: + name = 'editor/documents'; + case ImportType.databaseCSV: + name = 'editor/board'; + case ImportType.databaseRawData: + name = 'editor/board'; + case ImportType.markdownOrText: + name = 'editor/text'; + } + return FlowySvg( + name: name, + ); + }; + + bool get enableOnRelease { + switch (this) { + case ImportType.databaseRawData: + return kDebugMode; + default: + return true; + } + } + + List get allowedExtensions { + switch (this) { + case ImportType.historyDocument: + return ['afdoc']; + case ImportType.historyDatabase: + case ImportType.databaseRawData: + return ['afdb']; + case ImportType.markdownOrText: + return ['md', 'txt']; + case ImportType.databaseCSV: + return ['csv']; + } + } + + bool get allowMultiSelect { + switch (this) { + case ImportType.historyDocument: + case ImportType.databaseCSV: + case ImportType.databaseRawData: + case ImportType.historyDatabase: + case ImportType.markdownOrText: + return true; + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart new file mode 100644 index 0000000000..953e66437b --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart @@ -0,0 +1,121 @@ +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/app/app_bloc.dart'; +import 'package:provider/provider.dart'; +import 'section/section.dart'; + +class MenuApp extends StatefulWidget { + final ViewPB view; + const MenuApp(this.view, {Key? key}) : super(key: key); + + @override + State createState() => _MenuAppState(); +} + +class _MenuAppState extends State { + late ViewDataContext viewDataContext; + + @override + void initState() { + viewDataContext = ViewDataContext(viewId: widget.view.id); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + final appBloc = AppBloc(view: widget.view); + appBloc.add(const AppEvent.initial()); + return appBloc; + }, + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.latestCreatedView != c.latestCreatedView, + listener: (context, state) { + if (state.latestCreatedView != null) { + getIt().latestOpenView = + state.latestCreatedView; + } + }, + ), + BlocListener( + listener: (context, state) => viewDataContext.views = state.views, + ), + ], + child: BlocBuilder( + builder: (context, state) { + return ChangeNotifierProvider.value( + value: viewDataContext, + child: Consumer( + builder: (context, viewDataContext, _) { + return expandableWrapper(context, viewDataContext); + }, + ), + ); + }, + ), + ), + ); + } + + ExpandableNotifier expandableWrapper( + BuildContext context, + ViewDataContext viewDataContext, + ) { + return ExpandableNotifier( + controller: viewDataContext.expandController, + child: ScrollOnExpand( + scrollOnExpand: false, + scrollOnCollapse: false, + child: Column( + children: [ + ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToExpand: false, + tapBodyToCollapse: false, + tapHeaderToExpand: false, + iconPadding: EdgeInsets.zero, + hasIcon: false, + ), + header: MenuAppHeader(widget.view), + expanded: ViewSection(appViewData: viewDataContext), + collapsed: const SizedBox(), + ), + ], + ), + ), + ); + } + + @override + void didUpdateWidget(covariant MenuApp oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + viewDataContext.dispose(); + super.dispose(); + } +} + +class MenuAppSizes { + static double iconSize = 16; + static double headerHeight = 26; + static double headerPadding = 6; + static double iconPadding = 6; + static double appVPadding = 14; + static double scale = 1; + static double get expandedPadding => iconSize * scale + headerPadding; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart new file mode 100644 index 0000000000..acd8adf682 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart @@ -0,0 +1,215 @@ +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/presentation/home/menu/menu.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.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:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flowy_infra/image.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/icon_button.dart'; + +// ignore: must_be_immutable +class ViewSectionItem extends StatelessWidget { + final bool isSelected; + final ViewPB view; + final void Function(ViewPB) onSelected; + + const ViewSectionItem({ + Key? key, + required this.view, + required this.isSelected, + required this.onSelected, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (ctx) => getIt(param1: view) + ..add( + const ViewEvent.initial(), + ), + ), + ], + child: BlocBuilder( + builder: (blocContext, state) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: InkWell( + onTap: () => onSelected(blocContext.read().state.view), + child: FlowyHover( + style: HoverStyle( + hoverColor: Theme.of(context).colorScheme.secondary, + ), + // If current state.isEditing is true, the hover should not + // rebuild when onEnter/onExit events happened. + buildWhenOnHover: () => !state.isEditing, + builder: (_, onHover) => _render( + blocContext, + onHover, + state, + ), + isSelected: () => state.isEditing || isSelected, + ), + ), + ); + }, + ), + ); + } + + Widget _render( + BuildContext blocContext, + bool onHover, + ViewState state, + ) { + final List children = [ + SizedBox( + width: 16, + height: 16, + child: state.view.renderThumbnail(), + ), + const HSpace(2), + Expanded( + child: FlowyText.regular( + state.view.name, + overflow: TextOverflow.ellipsis, + ), + ), + ]; + + if (onHover || state.isEditing) { + children.add( + ViewDisclosureButton( + onEdit: (isEdit) => + blocContext.read().add(ViewEvent.setIsEditing(isEdit)), + onAction: (action) { + switch (action) { + case ViewDisclosureAction.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.disclosureAction_rename.tr(), + value: blocContext.read().state.view.name, + confirm: (newValue) { + blocContext + .read() + .add(ViewEvent.rename(newValue)); + }, + ).show(blocContext); + + break; + case ViewDisclosureAction.delete: + blocContext.read().add(const ViewEvent.delete()); + break; + case ViewDisclosureAction.duplicate: + blocContext.read().add(const ViewEvent.duplicate()); + break; + } + }, + ), + ); + } + + return SizedBox( + height: 26, + child: Row(children: children).padding( + left: MenuAppSizes.expandedPadding, + right: MenuAppSizes.headerPadding, + ), + ); + } +} + +enum ViewDisclosureAction { + rename, + delete, + duplicate, +} + +extension ViewDisclosureExtension on ViewDisclosureAction { + String get name { + switch (this) { + case ViewDisclosureAction.rename: + return LocaleKeys.disclosureAction_rename.tr(); + case ViewDisclosureAction.delete: + return LocaleKeys.disclosureAction_delete.tr(); + case ViewDisclosureAction.duplicate: + return LocaleKeys.disclosureAction_duplicate.tr(); + } + } + + Widget icon(Color iconColor) { + switch (this) { + case ViewDisclosureAction.rename: + return const FlowySvg(name: 'editor/edit'); + case ViewDisclosureAction.delete: + return const FlowySvg(name: 'editor/delete'); + case ViewDisclosureAction.duplicate: + return const FlowySvg(name: 'editor/copy'); + } + } +} + +class ViewDisclosureButton extends StatelessWidget { + final Function(bool) onEdit; + final Function(ViewDisclosureAction) onAction; + const ViewDisclosureButton({ + required this.onEdit, + required this.onAction, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + actions: ViewDisclosureAction.values + .map((action) => ViewDisclosureActionWrapper(action)) + .toList(), + buildChild: (controller) { + return FlowyIconButton( + hoverColor: Colors.transparent, + iconPadding: const EdgeInsets.all(5), + width: 26, + icon: svgWidget( + "editor/details", + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + onEdit(true); + controller.show(); + }, + ); + }, + onSelected: (action, controller) { + onEdit(false); + onAction(action.inner); + controller.close(); + }, + onClosed: () { + onEdit(false); + }, + ); + } +} + +class ViewDisclosureActionWrapper extends ActionCell { + final ViewDisclosureAction inner; + + ViewDisclosureActionWrapper(this.inner); + @override + Widget? leftIcon(Color iconColor) => inner.icon(iconColor); + + @override + String get name => inner.name; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart new file mode 100644 index 0000000000..780fd0d686 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/app/app_bloc.dart'; +import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:reorderables/reorderables.dart'; + +import 'item.dart'; + +class ViewSection extends StatelessWidget { + final ViewDataContext appViewData; + const ViewSection({Key? key, required this.appViewData}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + final bloc = ViewSectionBloc(appViewData: appViewData); + bloc.add(const ViewSectionEvent.initial()); + return bloc; + }, + child: BlocListener( + listenWhen: (p, c) => p.selectedView != c.selectedView, + listener: (context, state) { + if (state.selectedView != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + getIt().setPlugin( + state.selectedView!.plugin(listenOnViewChanged: true), + ); + }); + } + }, + child: BlocBuilder( + builder: (context, state) { + return _reorderableColumn(context, state); + }, + ), + ), + ); + } + + ReorderableColumn _reorderableColumn( + BuildContext context, + ViewSectionState state, + ) { + final children = state.views.map((view) { + final isSelected = _isViewSelected(state, view.id); + return ViewSectionItem( + view: view, + key: ValueKey('$view.hashCode/$isSelected'), + isSelected: isSelected, + onSelected: (view) => getIt().latestOpenView = view, + ); + }).toList(); + + return ReorderableColumn( + needsLongPressDraggable: false, + onReorder: (oldIndex, index) { + context + .read() + .add(ViewSectionEvent.moveView(oldIndex, index)); + }, + ignorePrimaryScrollController: true, + buildDraggableFeedback: (context, constraints, child) => ConstrainedBox( + constraints: constraints, + child: Material(color: Colors.transparent, child: child), + ), + children: children, + ); + } + + bool _isViewSelected(ViewSectionState state, String viewId) { + final view = state.selectedView; + if (view == null) { + return false; + } + return view.id == viewId; + } +} diff --git a/frontend/rust-lib/flowy-document/tests/assets/text/image.txt b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/favorite/favorite.dart similarity index 100% rename from frontend/rust-lib/flowy-document/tests/assets/text/image.txt rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/favorite/favorite.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/favorite/header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/favorite/header.dart new file mode 100644 index 0000000000..5907027044 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/favorite/header.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class FavoriteHeader extends StatelessWidget { + const FavoriteHeader({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} diff --git a/frontend/rust-lib/flowy-document/tests/file_storage.rs b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/favorite/section.dart similarity index 100% rename from frontend/rust-lib/flowy-document/tests/file_storage.rs rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/favorite/section.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu.dart new file mode 100644 index 0000000000..ec1ba4502c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu.dart @@ -0,0 +1,248 @@ +import 'dart:io' show Platform; + +import 'package:appflowy/core/frameless_window.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/trash/menu.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:easy_localization/easy_localization.dart'; +import 'package:expandable/expandable.dart'; +// import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/time/duration.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.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'; + +import '../navigation.dart'; +import 'app/create_button.dart'; +import 'app/menu_app.dart'; +import 'menu_user.dart'; + +export './app/header/header.dart'; +export './app/menu_app.dart'; + +class HomeMenu extends StatelessWidget { + final UserProfilePB user; + final WorkspaceSettingPB workspaceSetting; + + const HomeMenu({ + Key? key, + required this.user, + required this.workspaceSetting, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) { + final menuBloc = MenuBloc( + user: user, + workspace: workspaceSetting.workspace, + ); + menuBloc.add(const MenuEvent.initial()); + return menuBloc; + }, + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.plugin.id != c.plugin.id, + listener: (context, state) { + getIt().setPlugin(state.plugin); + }, + ), + ], + child: BlocBuilder( + builder: (context, state) => _renderBody(context), + ), + ), + ); + } + + Widget _renderBody(BuildContext context) { + // nested column: https://siddharthmolleti.com/flutter-box-constraints-nested-column-s-row-s-3dfacada7361 + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + border: + Border(right: BorderSide(color: Theme.of(context).dividerColor)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const MenuTopBar(), + const VSpace(10), + _renderApps(context), + ], + ).padding(horizontal: Insets.l), + ), + const VSpace(20), + const MenuTrash(), + const VSpace(20), + _renderNewAppButton(context), + ], + ), + ); + } + + Widget _renderApps(BuildContext context) { + return ExpandableTheme( + data: ExpandableThemeData( + useInkWell: true, + animationDuration: Durations.medium, + ), + child: Expanded( + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), + child: BlocSelector>( + selector: (state) => state.views + .map((app) => MenuApp(app, key: ValueKey(app.id))) + .toList(), + builder: (context, menuItems) { + return ReorderableListView.builder( + itemCount: menuItems.length, + buildDefaultDragHandles: false, + header: Padding( + padding: + EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding), + child: MenuUser(user), + ), + onReorder: (oldIndex, newIndex) { + // Moving item1 from index 0 to index 1 + // expect: oldIndex: 0, newIndex: 1 + // receive: oldIndex: 0, newIndex: 2 + // Workaround: if newIndex > oldIndex, we just minus one + final int index = newIndex > oldIndex ? newIndex - 1 : newIndex; + context + .read() + .add(MenuEvent.moveApp(oldIndex, index)); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return ReorderableDragStartListener( + key: ValueKey(menuItems[index].key), + index: index, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: MenuAppSizes.appVPadding / 2, + ), + child: menuItems[index], + ), + ); + }, + proxyDecorator: (child, index, animation) => + Material(color: Colors.transparent, child: child), + ); + }, + ), + ), + ), + ); + } + + Widget _renderNewAppButton(BuildContext context) { + return NewAppButton( + press: (appName) => + context.read().add(MenuEvent.createApp(appName, desc: "")), + ); + } +} + +class MenuSharedState { + final ValueNotifier _latestOpenView = ValueNotifier(null); + + MenuSharedState({ViewPB? view}) { + _latestOpenView.value = view; + } + + ViewPB? get latestOpenView => _latestOpenView.value; + ValueNotifier get notifier => _latestOpenView; + + set latestOpenView(ViewPB? view) { + if (_latestOpenView.value != view) { + _latestOpenView.value = view; + } + } + + VoidCallback addLatestViewListener(void Function(ViewPB?) callback) { + listener() { + callback(_latestOpenView.value); + } + + _latestOpenView.addListener(listener); + return listener; + } + + void removeLatestViewListener(VoidCallback listener) { + _latestOpenView.removeListener(listener); + } +} + +class MenuTopBar extends StatelessWidget { + const MenuTopBar({Key? key}) : super(key: key); + + Widget renderIcon(BuildContext context) { + if (Platform.isMacOS) { + return Container(); + } + return (Theme.of(context).brightness == Brightness.dark + ? svgWidget("flowy_logo_dark_mode", size: const Size(92, 17)) + : svgWidget("flowy_logo_with_text", size: const Size(92, 17))); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SizedBox( + height: HomeSizes.topBarHeight, + child: MoveWindowDetector( + child: Row( + children: [ + renderIcon(context), + const Spacer(), + Tooltip( + richMessage: sidebarTooltipTextSpan( + context, + LocaleKeys.sideBar_closeSidebar.tr(), + ), + child: FlowyIconButton( + width: 28, + hoverColor: Colors.transparent, + onPressed: () => context + .read() + .add(const HomeSettingEvent.collapseMenu()), + iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + icon: svgWidget( + "home/hide_menu", + color: Theme.of(context).iconTheme.color, + ), + ), + ) + ], + ), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_shared_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_shared_state.dart deleted file mode 100644 index 8c9d800470..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_shared_state.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - -class MenuSharedState { - MenuSharedState({ - ViewPB? view, - }) { - _latestOpenView.value = view; - } - - final ValueNotifier _latestOpenView = ValueNotifier(null); - - ViewPB? get latestOpenView => _latestOpenView.value; - ValueNotifier get notifier => _latestOpenView; - - set latestOpenView(ViewPB? view) { - if (_latestOpenView.value?.id != view?.id) { - _latestOpenView.value = view; - } - } - - void addLatestViewListener(VoidCallback listener) { - _latestOpenView.addListener(listener); - } - - void removeLatestViewListener(VoidCallback listener) { - _latestOpenView.removeListener(listener); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart new file mode 100644 index 0000000000..25f0f58bfc --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart @@ -0,0 +1,129 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class MenuUser extends StatelessWidget { + final UserProfilePB user; + MenuUser(this.user, {Key? key}) : super(key: ValueKey(user.id)); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + getIt(param1: user)..add(const MenuUserEvent.initial()), + child: BlocBuilder( + builder: (context, state) => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _renderAvatar(context), + const HSpace(10), + Expanded( + child: _renderUserName(context), + ), + _renderSettingsButton(context), + //ToDo: when the user is allowed to create another workspace, + //we get the below block back + //_renderDropButton(context), + ], + ), + ), + ); + } + + Widget _renderAvatar(BuildContext context) { + String iconUrl = context.read().state.userProfile.iconUrl; + if (iconUrl.isEmpty) { + iconUrl = defaultUserAvatar; + final String name = + userName(context.read().state.userProfile); + final Color color = ColorGenerator().generateColorFromString(name); + const initialsCount = 2; + // Taking the first letters of the name components and limiting to 2 elements + final nameInitials = name + .split(' ') + .where((element) => element.isNotEmpty) + .take(initialsCount) + .map((element) => element[0].toUpperCase()) + .join(''); + return Container( + width: 28, + height: 28, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: FlowyText.semibold( + nameInitials, + color: Colors.white, + fontSize: nameInitials.length == initialsCount ? 12 : 14, + ), + ); + } + return SizedBox( + width: 25, + height: 25, + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: svgWidget('emoji/$iconUrl'), + ), + ), + ); + } + + Widget _renderUserName(BuildContext context) { + final String name = userName(context.read().state.userProfile); + return FlowyText.medium( + name, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.tertiary, + ); + } + + Widget _renderSettingsButton(BuildContext context) { + final userProfile = context.read().state.userProfile; + return Tooltip( + message: LocaleKeys.settings_menu_open.tr(), + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return SettingsDialog(userProfile); + }, + ); + }, + icon: SizedBox.square( + dimension: 20, + child: svgWidget( + "home/settings", + color: Theme.of(context).colorScheme.tertiary, + ), + ), + ), + ); + } + + /// Return the user name, if the user name is empty, return the default user name. + String userName(UserProfilePB userProfile) { + String name = userProfile.name; + if (name.isEmpty) { + name = LocaleKeys.defaultUsername.tr(); + } + return name; + } +} 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 deleted file mode 100644 index ca0773bf72..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart +++ /dev/null @@ -1,219 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -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'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class FavoriteFolder extends StatefulWidget { - const FavoriteFolder({super.key, required this.views}); - - final List views; - - @override - State createState() => _FavoriteFolderState(); -} - -class _FavoriteFolderState extends State { - final isHovered = ValueNotifier(false); - - @override - void dispose() { - isHovered.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (widget.views.isEmpty) { - return const SizedBox.shrink(); - } - - return BlocProvider( - create: (context) => FolderBloc(type: FolderSpaceType.favorite) - ..add(const FolderEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return MouseRegion( - onEnter: (_) => isHovered.value = true, - onExit: (_) => isHovered.value = false, - child: Column( - children: [ - FavoriteHeader( - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - ), - buildReorderListView(context, state), - if (state.isExpanded) ...[ - // more button - const VSpace(2), - const FavoriteMoreButton(), - ], - ], - ), - ); - }, - ), - ); - } - - Widget buildReorderListView( - BuildContext context, - FolderState state, - ) { - 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); - } - - 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), - ), - ); - }, - onReorder: (oldIndex, newIndex) { - favoriteBloc.add(FavoriteEvent.reorder(oldIndex, newIndex)); - }, - ), - ); - } - - 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); - }, - ); - } -} - -class FavoriteHeader extends StatelessWidget { - const FavoriteHeader({super.key, required this.onPressed}); - - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: HomeSizes.newPageSectionHeight, - child: FlowyButton( - onTap: onPressed, - margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 3.0), - leftIcon: const FlowySvg( - FlowySvgs.favorite_header_icon_m, - blendMode: null, - ), - leftIconSize: const Size.square(24.0), - iconPadding: 8.0, - text: FlowyText.regular( - LocaleKeys.sideBar_favorites.tr(), - lineHeight: 1.15, - ), - ), - ); - } -} - -class FavoriteMoreButton extends StatelessWidget { - const FavoriteMoreButton({super.key}); - - @override - Widget build(BuildContext context) { - final favoriteBloc = context.watch(); - final tabsBloc = context.read(); - final unpinnedViews = favoriteBloc.state.unpinnedViews; - // only show the more button if there are unpinned views - if (unpinnedViews.isEmpty) { - return const SizedBox.shrink(); - } - - const minWidth = 260.0; - return AppFlowyPopover( - constraints: const BoxConstraints( - minWidth: minWidth, - ), - popupBuilder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: favoriteBloc), - BlocProvider.value(value: tabsBloc), - ], - child: const FavoriteMenu(minWidth: minWidth), - ), - margin: EdgeInsets.zero, - child: FlowyButton( - 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()), - ), - ); - } -} 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 deleted file mode 100644 index 1bf6635037..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart +++ /dev/null @@ -1,199 +0,0 @@ -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/tabs/tabs_bloc.dart'; -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: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 double _kHorizontalPadding = 10.0; -const double _kVerticalPadding = 10.0; - -class FavoriteMenu extends StatelessWidget { - const FavoriteMenu({super.key, required this.minWidth}); - - final double minWidth; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only( - left: _kHorizontalPadding, - right: _kHorizontalPadding, - top: _kVerticalPadding, - bottom: _kVerticalPadding, - ), - child: BlocProvider( - create: (context) => - FavoriteMenuBloc()..add(const FavoriteMenuEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(4), - SpaceSearchField( - width: minWidth - 2 * _kHorizontalPadding, - onSearch: (context, text) { - context - .read() - .add(FavoriteMenuEvent.search(text)); - }, - ), - const VSpace(12), - _FavoriteGroups( - minWidth: minWidth, - state: state, - ), - ], - ); - }, - ), - ), - ); - } -} - -class _FavoriteGroupedViews extends StatelessWidget { - const _FavoriteGroupedViews({ - required this.views, - }); - - final List views; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: views - .map( - (e) => ViewItem( - key: ValueKey(e.id), - view: e, - spaceType: FolderSpaceType.favorite, - level: 0, - onSelected: (_, view) { - context.read().openPlugin(view); - PopoverContainer.maybeOf(context)?.close(); - }, - isFeedback: false, - isDraggable: false, - shouldRenderChildren: false, - extendBuilder: (view) => view.isPinned - ? [ - const HSpace(4.0), - const FlowySvg( - FlowySvgs.favorite_pin_s, - blendMode: null, - ), - ] - : [], - leftIconBuilder: (_, __) => const HSpace(4.0), - rightIconsBuilder: (_, view) => [ - FavoriteMoreActions(view: view), - const HSpace(6.0), - FavoritePinAction(view: view), - const HSpace(4.0), - ], - ), - ) - .toList(), - ); - } -} - -class _FavoriteGroups extends StatelessWidget { - const _FavoriteGroups({ - required this.minWidth, - required this.state, - }); - - final double minWidth; - final FavoriteMenuState state; - - @override - Widget build(BuildContext context) { - final today = _buildGroups( - context, - state.todayViews, - LocaleKeys.sideBar_today.tr(), - ); - final thisWeek = _buildGroups( - context, - state.thisWeekViews, - LocaleKeys.sideBar_thisWeek.tr(), - ); - final others = _buildGroups( - context, - state.otherViews, - LocaleKeys.sideBar_others.tr(), - ); - - return Container( - width: minWidth - 2 * _kHorizontalPadding, - constraints: const BoxConstraints( - maxHeight: 300, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (today.isNotEmpty) ...[ - ...today, - ], - if (thisWeek.isNotEmpty) ...[ - if (today.isNotEmpty) ...[ - const FlowyDivider(), - const VSpace(16), - ], - ...thisWeek, - ], - if ((thisWeek.isNotEmpty || today.isNotEmpty) && - others.isNotEmpty) ...[ - const FlowyDivider(), - const VSpace(16), - ], - ...others.isNotEmpty && (today.isNotEmpty || thisWeek.isNotEmpty) - ? others - : _buildGroups( - context, - state.otherViews, - LocaleKeys.sideBar_others.tr(), - showHeader: false, - ), - ], - ), - ), - ); - } - - List _buildGroups( - BuildContext context, - List views, - String title, { - bool showHeader = true, - }) { - return [ - if (views.isNotEmpty) ...[ - if (showHeader) - FlowyText( - title, - fontSize: 12.0, - color: Theme.of(context).hintColor, - ), - const VSpace(2), - _FavoriteGroupedViews(views: views), - const VSpace(8), - ], - ]; - } -} 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 deleted file mode 100644 index 443e8a9840..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:appflowy/workspace/application/favorite/favorite_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'; - -part 'favorite_menu_bloc.freezed.dart'; - -class FavoriteMenuBloc extends Bloc { - FavoriteMenuBloc() : super(FavoriteMenuState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - final favoriteViews = await _service.readFavorites(); - List views = []; - List todayViews = []; - List thisWeekViews = []; - List otherViews = []; - - favoriteViews.onSuccess((s) { - _source = s; - (views, todayViews, thisWeekViews, otherViews) = _getViews(s); - }); - - emit( - state.copyWith( - views: views, - queriedViews: views, - todayViews: todayViews, - thisWeekViews: thisWeekViews, - otherViews: otherViews, - ), - ); - }, - search: (query) async { - if (_source == null) { - return; - } - var (views, todayViews, thisWeekViews, otherViews) = - _getViews(_source!); - var queriedViews = views; - - if (query.isNotEmpty) { - queriedViews = _filter(views, query); - todayViews = _filter(todayViews, query); - thisWeekViews = _filter(thisWeekViews, query); - otherViews = _filter(otherViews, query); - } - - emit( - state.copyWith( - views: views, - queriedViews: queriedViews, - todayViews: todayViews, - thisWeekViews: thisWeekViews, - otherViews: otherViews, - ), - ); - }, - ); - }, - ); - } - - final FavoriteService _service = FavoriteService(); - RepeatedFavoriteViewPB? _source; - - List _filter(List views, String query) => views - .where((view) => view.name.toLowerCase().contains(query.toLowerCase())) - .toList(); - - // all, today, last week, other - (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; - if (diff == 0) { - todayViews.add(view); - } else if (diff < 7) { - thisWeekViews.add(view); - } else { - otherViews.add(view); - } - } - - return (views, todayViews, thisWeekViews, otherViews); - } -} - -@freezed -class FavoriteMenuEvent with _$FavoriteMenuEvent { - const factory FavoriteMenuEvent.initial() = Initial; - const factory FavoriteMenuEvent.search(String query) = Search; -} - -@freezed -class FavoriteMenuState with _$FavoriteMenuState { - const factory FavoriteMenuState({ - @Default([]) List views, - @Default([]) List queriedViews, - @Default([]) List todayViews, - @Default([]) List thisWeekViews, - @Default([]) List otherViews, - }) = _FavoriteMenuState; - - factory FavoriteMenuState.initial() => const FavoriteMenuState(); -} 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 deleted file mode 100644 index 09b8a44842..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart +++ /dev/null @@ -1,79 +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/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'; -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 { - const FavoriteMoreActions({super.key, required this.view}); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), - child: ViewMoreActionPopover( - view: view, - spaceType: FolderSpaceType.favorite, - isExpanded: false, - onEditing: (value) => - context.read().add(ViewEvent.setIsEditing(value)), - onAction: (action, _) { - switch (action) { - case ViewMoreActionType.favorite: - case ViewMoreActionType.unFavorite: - context.read().add(FavoriteEvent.toggle(view)); - PopoverContainer.maybeOf(context)?.closeAll(); - break; - case ViewMoreActionType.rename: - NavigatorTextFieldDialog( - title: LocaleKeys.disclosureAction_rename.tr(), - autoSelectAllText: true, - value: view.nameOrDefault, - maxLength: 256, - onConfirm: (newValue, _) { - // can not use bloc here because it has been disposed. - ViewBackendService.updateView( - viewId: view.id, - name: newValue, - ); - }, - ).show(context); - PopoverContainer.maybeOf(context)?.closeAll(); - break; - - case ViewMoreActionType.openInNewTab: - getIt().openTab(view); - break; - case ViewMoreActionType.delete: - case ViewMoreActionType.duplicate: - default: - 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 deleted file mode 100644 index 3bd2ffe67f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/favorite/favorite_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/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class FavoritePinAction extends StatelessWidget { - const FavoritePinAction({super.key, required this.view}); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - final tooltip = view.isPinned - ? LocaleKeys.favorite_removeFromSidebar.tr() - : LocaleKeys.favorite_addToSidebar.tr(); - final icon = FlowySvg( - view.isPinned - ? FlowySvgs.favorite_section_unpin_s - : FlowySvgs.favorite_section_pin_s, - ); - return FlowyTooltip( - message: tooltip, - child: FlowyIconButton( - width: 24, - icon: icon, - onPressed: () { - view.isPinned - ? context.read().add(FavoriteEvent.unpin(view)) - : context.read().add(FavoriteEvent.pin(view)); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart deleted file mode 100644 index e4a335e34b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/workspace/application/favorite/favorite_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 'favorite_pin_bloc.freezed.dart'; - -class FavoritePinBloc extends Bloc { - FavoritePinBloc() : super(FavoritePinState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - final List views = await _service - .readFavorites() - .fold((s) => s.items.map((v) => v.item).toList(), (f) => []); - emit(state.copyWith(views: views, queriedViews: views)); - }, - search: (query) async { - if (query.isEmpty) { - emit(state.copyWith(queriedViews: state.views)); - return; - } - - final queriedViews = state.views - .where( - (view) => - view.name.toLowerCase().contains(query.toLowerCase()), - ) - .toList(); - emit(state.copyWith(queriedViews: queriedViews)); - }, - ); - }, - ); - } - - final FavoriteService _service = FavoriteService(); -} - -@freezed -class FavoritePinEvent with _$FavoritePinEvent { - const factory FavoritePinEvent.initial() = Initial; - const factory FavoritePinEvent.search(String query) = Search; -} - -@freezed -class FavoritePinState with _$FavoritePinState { - const factory FavoritePinState({ - @Default([]) List views, - @Default([]) List queriedViews, - @Default([]) List> todayViews, - @Default([]) List> lastWeekViews, - @Default([]) List> otherViews, - }) = _FavoritePinState; - - factory FavoritePinState.initial() => const FavoritePinState(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart deleted file mode 100644 index d73060d0b3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -class FolderHeader extends StatefulWidget { - const FolderHeader({ - super.key, - required this.title, - required this.expandButtonTooltip, - required this.addButtonTooltip, - required this.onPressed, - required this.onAdded, - required this.isExpanded, - }); - - final String title; - final String expandButtonTooltip; - final String addButtonTooltip; - final VoidCallback onPressed; - final VoidCallback onAdded; - final bool isExpanded; - - @override - State createState() => _FolderHeaderState(); -} - -class _FolderHeaderState extends State { - final isHovered = ValueNotifier(false); - - @override - void dispose() { - isHovered.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: HomeSizes.workspaceSectionHeight, - child: MouseRegion( - onEnter: (_) => isHovered.value = true, - onExit: (_) => isHovered.value = false, - child: FlowyButton( - onTap: widget.onPressed, - margin: const EdgeInsets.only(left: 6.0, right: 4.0), - rightIcon: ValueListenableBuilder( - valueListenable: isHovered, - builder: (context, onHover, child) => - Opacity(opacity: onHover ? 1 : 0, child: child), - child: FlowyIconButton( - width: 24, - iconPadding: const EdgeInsets.all(4.0), - tooltipText: widget.addButtonTooltip, - icon: const FlowySvg(FlowySvgs.view_item_add_s), - onPressed: widget.onAdded, - ), - ), - iconPadding: 10.0, - text: Row( - children: [ - FlowyText( - widget.title, - lineHeight: 1.15, - ), - const HSpace(4.0), - FlowySvg( - widget.isExpanded - ? FlowySvgs.workspace_drop_down_menu_show_s - : FlowySvgs.workspace_drop_down_menu_hide_s, - ), - ], - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index a8717e28bc..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart +++ /dev/null @@ -1,145 +0,0 @@ -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/view/view_item.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'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SectionFolder extends StatefulWidget { - const SectionFolder({ - super.key, - required this.title, - required this.spaceType, - required this.views, - this.isHoverEnabled = true, - required this.expandButtonTooltip, - required this.addButtonTooltip, - }); - - final String title; - final FolderSpaceType spaceType; - final List views; - final bool isHoverEnabled; - final String expandButtonTooltip; - final String addButtonTooltip; - - @override - State createState() => _SectionFolderState(); -} - -class _SectionFolderState extends State { - final 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: 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), - ], - ), - ), - ), - ); - } - - Widget _buildHeader(BuildContext context) { - return FolderHeader( - title: widget.title, - isExpanded: context.watch().state.isExpanded, - expandButtonTooltip: widget.expandButtonTooltip, - addButtonTooltip: widget.addButtonTooltip, - onPressed: () => - context.read().add(const FolderEvent.expandOrUnExpand()), - onAdded: () { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: '', - index: 0, - viewSection: widget.spaceType.toViewSectionPB, - ), - ); - - context - .read() - .add(const FolderEvent.expandOrUnExpand(isExpanded: true)); - }, - ); - } - - Iterable _buildViews( - BuildContext context, - FolderState state, - ValueNotifier isHovered, - ) { - if (!state.isExpanded) { - return []; - } - - return widget.views.map( - (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); - } - - context.read().openPlugin(view); - }, - onTertiarySelected: (viewContext, view) => - context.read().openTab(view), - isHoverEnabled: widget.isHoverEnabled, - ), - ); - } - - Widget _buildDraggablePlaceholder(BuildContext context) { - if (widget.views.isNotEmpty) { - return const SizedBox.shrink(); - } - final parentViewId = - context.read().state.currentWorkspace?.workspaceId; - return ViewItem( - spaceType: widget.spaceType, - view: ViewPB(parentViewId: parentViewId ?? ''), - level: 0, - leftPadding: HomeSpaceViewSizes.leftPadding, - isFeedback: false, - onSelected: (_, __) {}, - 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 deleted file mode 100644 index f8c3a30488..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart +++ /dev/null @@ -1,111 +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/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/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:flutter/material.dart'; - -import 'sidebar_footer_button.dart'; - -class SidebarFooter extends StatelessWidget { - const SidebarFooter({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - 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()), - ], - ), - ], - ); - } - - 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}); - - @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), - ), - ); - }, - ); - }, - ); - } -} - -class SidebarWidgetButton extends StatelessWidget { - const SidebarWidgetButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () {}, - child: const FlowySvg(FlowySvgs.sidebar_footer_widget_s), - ), - ); - } -} 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 deleted file mode 100644 index 67930c336a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart +++ /dev/null @@ -1,108 +0,0 @@ -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/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: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. -/// -/// in the top menu, we have: -/// - appflowy icon (Windows or Linux) -/// - close / expand sidebar button -class SidebarTopMenu extends StatelessWidget { - const SidebarTopMenu({ - super.key, - required this.isSidebarOnHover, - }); - - final ValueNotifier isSidebarOnHover; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, _) => SizedBox( - height: !UniversalPlatform.isWindows ? HomeSizes.topBarHeight : 45, - child: MoveWindowDetector( - child: Row( - children: [ - _buildLogoIcon(context), - const Spacer(), - _buildCollapseMenuButton(context), - ], - ), - ), - ), - ); - } - - Widget _buildLogoIcon(BuildContext context) { - if (Platform.isMacOS) { - return const SizedBox.shrink(); - } - - final svgData = Theme.of(context).brightness == Brightness.dark - ? FlowySvgs.app_logo_with_text_dark_xl - : FlowySvgs.app_logo_with_text_light_xl; - - return Padding( - padding: const EdgeInsets.only(top: 12.0, left: 8), - child: FlowySvg( - svgData, - size: const Size(92, 17), - blendMode: null, - ), - ); - } - - Widget _buildCollapseMenuButton(BuildContext context) { - final textSpan = TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), - ), - ], - ); - - return ValueListenableBuilder( - valueListenable: isSidebarOnHover, - builder: (_, value, ___) => Opacity( - opacity: value ? 1 : 0, - child: Padding( - padding: const EdgeInsets.only(top: 12.0, right: 6.0), - child: 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 FlowySvg(FlowySvgs.hide_menu_s), - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart deleted file mode 100644 index 524934aa82..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; -import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' - show UserProfilePB; -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:flutter_bloc/flutter_bloc.dart'; - -// keep this widget in case we need to roll back (lucas.xu) -class SidebarUser extends StatelessWidget { - const SidebarUser({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => - MenuUserBloc(userProfile)..add(const MenuUserEvent.initial()), - child: BlocBuilder( - builder: (context, state) => Row( - children: [ - const HSpace(4), - UserAvatar( - iconUrl: state.userProfile.iconUrl, - name: state.userProfile.name, - size: 24.0, - fontSize: 16.0, - decoration: ShapeDecoration( - color: const Color(0xFFFBE8FB), - shape: RoundedRectangleBorder( - side: const BorderSide(width: 0.50, color: Color(0x19171717)), - borderRadius: BorderRadius.circular(8), - ), - ), - ), - const HSpace(8), - Expanded(child: _buildUserName(context, state)), - UserSettingButton(userProfile: state.userProfile), - const HSpace(8.0), - const NotificationButton(), - const HSpace(10.0), - ], - ), - ), - ); - } - - Widget _buildUserName(BuildContext context, MenuUserState state) { - final String name = _userName(state.userProfile); - return FlowyText.medium( - name, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).colorScheme.tertiary, - fontSize: 15.0, - ); - } - - /// Return the user name, if the user name is empty, return the default user name. - String _userName(UserProfilePB userProfile) { - String name = userProfile.name; - if (name.isEmpty) { - name = LocaleKeys.defaultUsername.tr(); - } - return name; - } -} 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 deleted file mode 100644 index 716002e917..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart +++ /dev/null @@ -1,234 +0,0 @@ -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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/container.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:path/path.dart' as p; - -typedef ImportCallback = void Function( - ImportType type, - String name, - List? document, -); - -Future showImportPanel( - String parentViewId, - BuildContext context, - ImportCallback callback, -) async { - await FlowyOverlay.show( - context: context, - builder: (context) => FlowyDialog( - backgroundColor: Theme.of(context).colorScheme.surface, - title: FlowyText.semibold( - LocaleKeys.moreAction_import.tr(), - fontSize: 20, - color: Theme.of(context).colorScheme.tertiary, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 20.0, - ), - child: ImportPanel( - parentViewId: parentViewId, - importCallback: callback, - ), - ), - ), - ); -} - -class ImportPanel extends StatefulWidget { - const ImportPanel({ - super.key, - required this.parentViewId, - required this.importCallback, - }); - - final String parentViewId; - final ImportCallback importCallback; - - @override - State createState() => _ImportPanelState(); -} - -class _ImportPanelState extends State { - final flowyContainerFocusNode = FocusNode(); - final ValueNotifier showLoading = ValueNotifier(false); - - @override - void dispose() { - flowyContainerFocusNode.dispose(); - showLoading.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final width = MediaQuery.of(context).size.width * 0.7; - final height = width * 0.5; - return KeyboardListener( - autofocus: true, - focusNode: flowyContainerFocusNode, - onKeyEvent: (event) { - if (event is KeyDownEvent && - event.physicalKey == PhysicalKeyboardKey.escape) { - 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); - } - }, - ), - ), - ) - .toList(), - ), - ), - ValueListenableBuilder( - valueListenable: showLoading, - builder: (context, showLoading, child) { - if (!showLoading) { - return const SizedBox.shrink(); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, - ), - ], - ), - ); - } - - Future _importFile(String parentViewId, ImportType importType) async { - final result = await getIt().pickFiles( - type: FileType.custom, - allowMultiple: importType.allowMultiSelect, - allowedExtensions: importType.allowedExtensions, - ); - if (result == null || result.files.isEmpty) { - return; - } - - showLoading.value = true; - - final importValues = []; - for (final file in result.files) { - final path = file.path; - if (path == null) { - continue; - } - 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(); - final bytes = _documentDataFrom(importType, data); - if (bytes != null) { - importValues.add( - ImportItemPayloadPB.create() - ..name = name - ..data = bytes - ..viewLayout = ViewLayoutPB.Document - ..importType = ImportTypePB.Markdown, - ); - } - 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, - ); - 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, - ); - break; - } - } - - 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.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 deleted file mode 100644 index 5c7c297327..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_type.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:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -enum ImportType { - historyDocument, - historyDatabase, - markdownOrText, - csv, - afDatabase; - - @override - String toString() { - switch (this) { - case ImportType.historyDocument: - return LocaleKeys.importPanel_documentFromV010.tr(); - case ImportType.historyDatabase: - return LocaleKeys.importPanel_databaseFromV010.tr(); - case ImportType.markdownOrText: - return LocaleKeys.importPanel_textAndMarkdown.tr(); - case ImportType.csv: - return LocaleKeys.importPanel_csv.tr(); - case ImportType.afDatabase: - return LocaleKeys.importPanel_database.tr(); - } - } - - WidgetBuilder get icon => (context) { - final FlowySvgData svg; - switch (this) { - case ImportType.historyDatabase: - svg = FlowySvgs.document_s; - case ImportType.historyDocument: - case ImportType.csv: - case ImportType.afDatabase: - svg = FlowySvgs.board_s; - case ImportType.markdownOrText: - svg = FlowySvgs.text_s; - } - - return FlowySvg( - svg, - color: Theme.of(context).colorScheme.tertiary, - ); - }; - - bool get enableOnRelease { - switch (this) { - case ImportType.historyDatabase: - case ImportType.historyDocument: - case ImportType.afDatabase: - return kDebugMode; - default: - return true; - } - } - - List get allowedExtensions { - switch (this) { - case ImportType.historyDocument: - return ['afdoc']; - case ImportType.historyDatabase: - case ImportType.afDatabase: - return ['afdb']; - case ImportType.markdownOrText: - return ['md', 'txt']; - case ImportType.csv: - return ['csv']; - } - } - - bool get allowMultiSelect { - switch (this) { - case ImportType.historyDocument: - 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/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart deleted file mode 100644 index c27f259b68..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart +++ /dev/null @@ -1,113 +0,0 @@ -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/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.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/folder/_section_folder.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 SidebarFolder extends StatelessWidget { - const SidebarFolder({ - super.key, - this.isHoverEnabled = true, - required this.userProfile, - }); - - final bool isHoverEnabled; - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - const sectionPadding = 16.0; - return ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, child) { - return 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(), - ); - }, - ), - // public or private - BlocBuilder( - builder: (context, state) { - // only show public and private section if the workspace is collaborative and not local - final isCollaborativeWorkspace = - context.read().state.isCollabWorkspaceOn; - - // only show public and private section if the workspace is collaborative - return Column( - children: isCollaborativeWorkspace - ? [ - // public - const VSpace(sectionPadding), - PublicSectionFolder(views: state.section.publicViews), - - // private - const VSpace(sectionPadding), - PrivateSectionFolder( - views: state.section.privateViews, - ), - ] - : [ - // personal - const VSpace(sectionPadding), - PersonalSectionFolder( - views: state.section.publicViews, - ), - ], - ); - }, - ), - const VSpace(200), - ], - ); - }, - ); - } -} - -class PrivateSectionFolder extends SectionFolder { - PrivateSectionFolder({super.key, required super.views}) - : super( - title: LocaleKeys.sideBar_private.tr(), - spaceType: FolderSpaceType.private, - expandButtonTooltip: LocaleKeys.sideBar_clickToHidePrivate.tr(), - addButtonTooltip: LocaleKeys.sideBar_addAPageToPrivate.tr(), - ); -} - -class PublicSectionFolder extends SectionFolder { - PublicSectionFolder({super.key, required super.views}) - : super( - title: LocaleKeys.sideBar_workspace.tr(), - spaceType: FolderSpaceType.public, - expandButtonTooltip: LocaleKeys.sideBar_clickToHideWorkspace.tr(), - addButtonTooltip: LocaleKeys.sideBar_addAPageToWorkspace.tr(), - ); -} - -class PersonalSectionFolder extends SectionFolder { - PersonalSectionFolder({super.key, required super.views}) - : super( - title: LocaleKeys.sideBar_personal.tr(), - spaceType: FolderSpaceType.public, - expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), - addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(), - ); -} 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 deleted file mode 100644 index d35c4cd148..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.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/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_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 { - 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(), - leftIcon: const FlowySvg( - FlowySvgs.new_app_m, - blendMode: null, - ), - leftIconSize: const Size.square(24.0), - margin: const EdgeInsets.only(left: 4.0), - iconPadding: 8.0, - text: FlowyText.regular( - LocaleKeys.newPageText.tr(), - lineHeight: 1.15, - ), - ), - ); - } - - 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, - ), - ); - } - } -} 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 deleted file mode 100644 index 0bd5dafe91..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart +++ /dev/null @@ -1,168 +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_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'; -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: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:hotkey_manager/hotkey_manager.dart'; -import 'package:universal_platform/universal_platform.dart'; - -final GlobalKey _settingsDialogKey = GlobalKey(); - -HotKeyItem openSettingsHotKey( - BuildContext context, - UserProfilePB userProfile, -) => - HotKeyItem( - hotKey: HotKey( - KeyCode.comma, - scope: HotKeyScope.inapp, - modifiers: [ - UniversalPlatform.isMacOS ? KeyModifier.meta : KeyModifier.control, - ], - ), - keyDownHandler: (_) { - if (_settingsDialogKey.currentContext == null) { - showSettingsDialog(context, userProfile: userProfile); - } else { - Navigator.of(context, rootNavigator: true) - .popUntil((route) => route.isFirst); - } - }, - ); - -class UserSettingButton extends StatefulWidget { - const UserSettingButton({ - super.key, - required this.userProfile, - this.isHover = false, - }); - - final UserProfilePB userProfile; - final bool isHover; - - @override - State createState() => _UserSettingButtonState(); -} - -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, - ), - ), - ), - ), - ); - } -} - -void showSettingsDialog( - BuildContext context, { - required UserProfilePB userProfile, - UserWorkspaceBloc? userWorkspaceBloc, - PasswordBloc? passwordBloc, - SettingsPage? initPage, -}) { - AFFocusManager.maybeOf(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(), - ), - ], - 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(); - }, - ), - ), - ); -} 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 deleted file mode 100644 index 9c19184217..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ /dev/null @@ -1,528 +0,0 @@ -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'; -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'; -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'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_migration.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -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: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: -/// - user icon, user name -/// - settings -/// - scrollable document list -/// - trash -class HomeSideBar extends StatelessWidget { - const HomeSideBar({ - super.key, - required this.userProfile, - required this.workspaceSetting, - }); - - final UserProfilePB userProfile; - - final WorkspaceLatestPB workspaceSetting; - - @override - Widget build(BuildContext context) { - // Workspace Bloc: control the current workspace - // | - // +-- Workspace Menu - // | | - // | +-- Workspace List: control to switch workspace - // | | - // | +-- Workspace Settings - // | | - // | +-- Notification Center - // | - // +-- Favorite Section - // | - // +-- 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(), - ), - ), - ), - 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())); - } else { - context.read().add( - TabsEvent.openPlugin( - plugin: state.lastCreatedPage!.plugin(), - ), - ); - } - - 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), - ), - ); - }, - ), - ); - } - - void _onNotificationAction( - BuildContext context, - ActionNavigationState state, - ) { - final action = state.action; - if (action?.type == ActionType.openView) { - final view = action!.arguments?[ActionArgumentKeys.view]; - if (view != null) { - final Map arguments = {}; - final nodePath = action.arguments?[ActionArgumentKeys.nodePath]; - if (nodePath != null) { - arguments[PluginArgumentKeys.selection] = Selection.collapsed( - Position(path: [nodePath]), - ); - } - - 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; - } - - context.read().openPlugin(view, arguments: arguments); - } - } - } -} - -class _Sidebar extends StatefulWidget { - const _Sidebar({required this.userProfile}); - - final UserProfilePB userProfile; - - @override - State<_Sidebar> createState() => _SidebarState(); -} - -class _SidebarState extends State<_Sidebar> { - final _scrollController = ScrollController(); - Timer? _scrollDebounce; - bool _isScrolling = false; - 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(); - _scrollController.addListener(_onScrollChanged); - } - - @override - void dispose() { - _scrollDebounce?.cancel(); - _scrollController.removeListener(_onScrollChanged); - _scrollController.dispose(); - _scrollOffset.dispose(); - _isHovered.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 8); - return MouseRegion( - onEnter: (_) => _isHovered.value = true, - onExit: (_) => _isHovered.value = false, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - border: Border( - right: BorderSide(color: Theme.of(context).dividerColor), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // top menu - Padding( - padding: menuHorizontalInset, - child: SidebarTopMenu( - isSidebarOnHover: _isHovered, - ), - ), - // 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), - ), - ), - if (FeatureFlag.search.isOn) ...[ - const VSpace(6), - Container( - padding: menuHorizontalInset, - height: HomeSizes.searchSectionHeight, - child: const _SidebarSearchButton(), - ), - ], - const VSpace(6.0), - // new page button - const SidebarNewPageButton(), - // scrollable document list - const VSpace(12.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: ValueListenableBuilder( - valueListenable: _scrollOffset, - builder: (_, offset, child) => Opacity( - opacity: offset > 0 ? 1 : 0, - child: child, - ), - child: const FlowyDivider(), - ), - ), - - _renderFolderOrSpace(menuHorizontalInset), - - // trash - Padding( - padding: menuHorizontalInset + - const EdgeInsets.symmetric(horizontal: 4.0), - child: const FlowyDivider(), - ), - const VSpace(8), - - _renderUpgradeSpaceButton(menuHorizontalInset), - _buildUpgradeApplicationButton(menuHorizontalInset), - - const VSpace(8), - Padding( - padding: menuHorizontalInset + - const EdgeInsets.symmetric(horizontal: 4.0), - child: const SidebarFooter(), - ), - const VSpace(14), - ], - ), - ), - ); - } - - 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 - ? Expanded( - child: Padding( - padding: menuHorizontalInset - const EdgeInsets.only(right: 6), - child: SingleChildScrollView( - padding: const EdgeInsets.only(right: 6), - controller: _scrollController, - physics: const ClampingScrollPhysics(), - child: SidebarFolder( - userProfile: widget.userProfile, - isHoverEnabled: !_isScrolling, - ), - ), - ), - ) - : Expanded( - child: Padding( - padding: menuHorizontalInset - const EdgeInsets.only(right: 6), - child: FlowyScrollbar( - controller: _scrollController, - child: SingleChildScrollView( - padding: const EdgeInsets.only(right: 6), - controller: _scrollController, - physics: const ClampingScrollPhysics(), - child: SidebarSpace( - userProfile: widget.userProfile, - isHoverEnabled: !_isScrolling, - ), - ), - ), - ), - ); - } - - Widget _renderUpgradeSpaceButton(EdgeInsets menuHorizontalInset) { - final spaceState = context.watch().state; - final workspaceState = context.read().state; - return !spaceState.shouldShowUpgradeDialog || - !workspaceState.isCollabWorkspaceOn - ? const SizedBox.shrink() - : Padding( - padding: menuHorizontalInset + - const EdgeInsets.only( - left: 4.0, - right: 4.0, - top: 8.0, - ), - child: const SpaceMigration(), - ); - } - - 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); - - _scrollDebounce?.cancel(); - _scrollDebounce = - Timer(const Duration(milliseconds: 300), _setScrollStopped); - - _scrollOffset.value = _scrollController.offset; - } - - void _setScrollStopped() { - if (mounted) { - setState(() => _isScrolling = false); - } - } -} - -class _SidebarSearchButton extends StatelessWidget { - const _SidebarSearchButton(); - - @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()), - ), - ); - } -} 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 deleted file mode 100644 index e3ce26e835..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart +++ /dev/null @@ -1,131 +0,0 @@ -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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class CreateSpacePopup extends StatefulWidget { - const CreateSpacePopup({super.key}); - - @override - State createState() => _CreateSpacePopupState(); -} - -class _CreateSpacePopupState extends State { - String spaceName = LocaleKeys.space_defaultSpaceName.tr(); - String? spaceIcon = kDefaultSpaceIconId; - 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, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - LocaleKeys.space_createNewSpace.tr(), - fontSize: 18.0, - figmaLineHeight: 24.0, - ), - const VSpace(2.0), - FlowyText( - LocaleKeys.space_createSpaceDescription.tr(), - fontSize: 14.0, - fontWeight: FontWeight.w300, - color: Theme.of(context).hintColor, - figmaLineHeight: 18.0, - maxLines: 2, - ), - const VSpace(16.0), - SizedBox.square( - dimension: 56, - child: SpaceIconPopup( - onIconChanged: (icon, iconColor) { - spaceIcon = icon; - spaceIconColor = iconColor; - }, - ), - ), - const VSpace(8.0), - _SpaceNameTextField( - onChanged: (value) => spaceName = value, - onSubmitted: (value) { - spaceName = value; - _createSpace(); - }, - ), - const VSpace(20.0), - SpacePermissionSwitch( - onPermissionChanged: (value) => spacePermission = value, - ), - const VSpace(20.0), - SpaceCancelOrConfirmButton( - confirmButtonName: LocaleKeys.button_create.tr(), - onCancel: () => Navigator.of(context).pop(), - onConfirm: () => _createSpace(), - ), - ], - ), - ); - } - - void _createSpace() { - context.read().add( - SpaceEvent.create( - name: spaceName, - // fixme: space issue - icon: spaceIcon!, - iconColor: spaceIconColor!, - permission: spacePermission, - createNewPageByDefault: true, - openAfterCreate: true, - ), - ); - - Navigator.of(context).pop(); - } -} - -class _SpaceNameTextField extends StatelessWidget { - const _SpaceNameTextField({ - required this.onChanged, - required this.onSubmitted, - }); - - final void Function(String name) onChanged; - final void Function(String name) onSubmitted; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.regular( - 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(), - 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 deleted file mode 100644 index eb8c54025d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.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/shared_widget.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ManageSpacePopup extends StatefulWidget { - const ManageSpacePopup({super.key}); - - @override - State createState() => _ManageSpacePopupState(); -} - -class _ManageSpacePopupState extends State { - String? spaceName; - String? spaceIcon; - String? spaceIconColor; - SpacePermission? spacePermission; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - width: 500, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - LocaleKeys.space_manage.tr(), - fontSize: 18.0, - ), - const VSpace(16.0), - _SpaceNameTextField( - onNameChanged: (name) => spaceName = name, - onIconChanged: (icon, color) { - spaceIcon = icon; - spaceIconColor = color; - }, - ), - const VSpace(16.0), - SpacePermissionSwitch( - spacePermission: - context.read().state.currentSpace?.spacePermission, - onPermissionChanged: (value) => spacePermission = value, - ), - const VSpace(16.0), - SpaceCancelOrConfirmButton( - confirmButtonName: LocaleKeys.button_save.tr(), - onCancel: () => Navigator.of(context).pop(), - onConfirm: () { - context.read().add( - SpaceEvent.update( - name: spaceName, - icon: spaceIcon, - iconColor: spaceIconColor, - permission: spacePermission, - ), - ); - - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } -} - -class _SpaceNameTextField extends StatelessWidget { - const _SpaceNameTextField({ - required this.onNameChanged, - required this.onIconChanged, - }); - - final void Function(String name) onNameChanged; - final void Function(String? icon, String? color) onIconChanged; - - @override - Widget build(BuildContext context) { - final space = context.read().state.currentSpace; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.regular( - LocaleKeys.space_spaceName.tr(), - fontSize: 14.0, - color: Theme.of(context).hintColor, - ), - const VSpace(8.0), - SizedBox( - height: 40, - child: Row( - children: [ - SizedBox.square( - dimension: 40, - child: SpaceIconPopup( - space: space, - cornerRadius: 12, - icon: space?.spaceIcon, - iconColor: space?.spaceIconColor, - onIconChanged: onIconChanged, - ), - ), - const HSpace(12), - Expanded( - child: SizedBox( - height: 40, - child: FlowyTextField( - text: space?.name, - onChanged: onNameChanged, - ), - ), - ), - ], - ), - ), - ], - ); - } -} 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 deleted file mode 100644 index d06016dfb8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ /dev/null @@ -1,698 +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: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: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: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({ - super.key, - required this.onPermissionChanged, - this.spacePermission, - this.showArrow = false, - }); - - final SpacePermission? spacePermission; - final void Function(SpacePermission permission) onPermissionChanged; - final bool showArrow; - - @override - State createState() => _SpacePermissionSwitchState(); -} - -class _SpacePermissionSwitchState extends State { - late SpacePermission spacePermission = - widget.spacePermission ?? SpacePermission.publicToAll; - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.regular( - LocaleKeys.space_permission.tr(), - fontSize: 14.0, - color: Theme.of(context).hintColor, - figmaLineHeight: 18.0, - ), - const VSpace(6.0), - AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints(maxWidth: 500), - offset: const Offset(0, 4), - margin: EdgeInsets.zero, - popupBuilder: (_) => _buildPermissionButtons(), - child: DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide(color: context.enableBorderColor), - borderRadius: BorderRadius.circular(10), - ), - ), - child: SpacePermissionButton( - showArrow: true, - permission: spacePermission, - ), - ), - ), - ], - ); - } - - Widget _buildPermissionButtons() { - return SizedBox( - width: 452, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SpacePermissionButton( - permission: SpacePermission.publicToAll, - onTap: () => _onPermissionChanged(SpacePermission.publicToAll), - ), - SpacePermissionButton( - permission: SpacePermission.private, - onTap: () => _onPermissionChanged(SpacePermission.private), - ), - ], - ), - ); - } - - void _onPermissionChanged(SpacePermission permission) { - widget.onPermissionChanged(permission); - - setState(() { - spacePermission = permission; - }); - - popoverController.close(); - } -} - -class SpacePermissionButton extends StatelessWidget { - const SpacePermissionButton({ - super.key, - required this.permission, - this.onTap, - this.showArrow = false, - }); - - final SpacePermission permission; - final VoidCallback? onTap; - final bool showArrow; - - @override - Widget build(BuildContext context) { - final (title, desc, icon) = switch (permission) { - SpacePermission.publicToAll => ( - LocaleKeys.space_publicPermission.tr(), - LocaleKeys.space_publicPermissionDescription.tr(), - FlowySvgs.space_permission_public_s - ), - SpacePermission.private => ( - LocaleKeys.space_privatePermission.tr(), - LocaleKeys.space_privatePermissionDescription.tr(), - FlowySvgs.space_permission_private_s - ), - }; - - return FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0), - 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: [ - FlowyText.regular(title), - const VSpace(4.0), - FlowyText.regular( - desc, - fontSize: 12.0, - color: Theme.of(context).hintColor, - ), - ], - ), - onTap: onTap, - ); - } -} - -class SpaceCancelOrConfirmButton extends StatelessWidget { - const SpaceCancelOrConfirmButton({ - super.key, - required this.onCancel, - required this.onConfirm, - required this.confirmButtonName, - this.confirmButtonColor, - }); - - final VoidCallback onCancel; - final VoidCallback onConfirm; - final String confirmButtonName; - final Color? confirmButtonColor; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedRoundedButton( - text: LocaleKeys.button_cancel.tr(), - onTap: onCancel, - ), - const HSpace(12.0), - 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, - ), - ), - ], - ); - } -} - -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, - ), - ], - ); - } -} - -enum ConfirmPopupStyle { - onlyOk, - cancelAndOk, -} - -class ConfirmPopupColor { - static Color titleColor(BuildContext context) { - if (Theme.of(context).isLightMode) { - return const Color(0xFF171717).withValues(alpha: 0.8); - } - return const Color(0xFFffffff).withValues(alpha: 0.8); - } - - static Color descriptionColor(BuildContext context) { - if (Theme.of(context).isLightMode) { - return const Color(0xFF171717).withValues(alpha: 0.7); - } - return const Color(0xFFffffff).withValues(alpha: 0.7); - } -} - -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.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; - - @override - State createState() => _ConfirmPopupState(); -} - -class _ConfirmPopupState extends State { - final focusNode = FocusNode(); - - @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), - ], - ), - ), - ); - } - - Widget _buildTitle() { - return Row( - children: [ - Expanded( - child: FlowyText( - widget.title, - fontSize: 16.0, - figmaLineHeight: 22.0, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - color: ConfirmPopupColor.titleColor(context), - ), - ), - const HSpace(6.0), - if (widget.showCloseButton) ...[ - 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 _buildDescription() { - if (widget.description.isEmpty) { - return const SizedBox.shrink(); - } - - return FlowyText.regular( - widget.description, - fontSize: 16.0, - color: ConfirmPopupColor.descriptionColor(context), - maxLines: 5, - figmaLineHeight: 22.0, - ); - } - - Widget _buildStyledButton(BuildContext context) { - switch (widget.style) { - case ConfirmPopupStyle.onlyOk: - 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, - ); - } - } -} - -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( - children: [ - Expanded( - flex: 2, - child: FlowyHover( - child: Padding( - padding: const EdgeInsets.all(2.0), - child: child, - ), - ), - ), - Expanded( - child: FlowyTooltip( - message: LocaleKeys.space_movePageToSpace.tr(), - child: GestureDetector( - onTap: onTapBlankArea, - ), - ), - ), - ], - ); - } - - 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 deleted file mode 100644 index e4be64d5b9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/favorite/favorite_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/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/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_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: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:provider/provider.dart'; - -class SidebarSpace extends StatelessWidget { - const SidebarSpace({ - super.key, - this.isHoverEnabled = true, - required this.userProfile, - }); - - final bool isHoverEnabled; - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - 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), - ], - ), - ), - ); - } -} - -class _Space extends StatefulWidget { - const _Space(); - - @override - State<_Space> createState() => _SpaceState(); -} - -class _SpaceState extends State<_Space> { - final isHovered = ValueNotifier(false); - final isExpandedNotifier = PropertyValueNotifier(false); - - @override - void initState() { - super.initState(); - switchToTheNextSpace.addListener(_switchToNextSpace); - } - - @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) { - return const SizedBox.shrink(); - } - - final currentSpace = state.currentSpace ?? state.spaces.first; - - return Column( - children: [ - SidebarSpaceHeader( - isExpanded: state.isExpanded, - space: currentSpace, - onAdded: (layout) => _showCreatePagePopup( - context, - currentSpace, - layout, - ), - onCreateNewSpace: () => _showCreateSpaceDialog(context), - onCollapseAllPages: () => isExpandedNotifier.value = true, - ), - if (state.isExpanded) - MouseRegion( - onEnter: (_) => isHovered.value = true, - onExit: (_) => isHovered.value = false, - child: SpacePages( - key: ValueKey( - Object.hashAll([ - currentWorkspace?.workspaceId ?? '', - 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), - ), - ), - ], - ); - }, - ); - } - - void _showCreateSpaceDialog(BuildContext context) { - final spaceBloc = context.read(); - showDialog( - context: context, - builder: (_) => 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, - ), - ); - - context.read().add(SpaceEvent.expand(space, true)); - } - - void _switchToNextSpace() { - context.read().add(const SpaceEvent.switchToNextSpace()); - } -} 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 deleted file mode 100644 index cf4a2aa5b1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -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/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/space_action_type.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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart' hide Icon; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SidebarSpaceHeader extends StatefulWidget { - const SidebarSpaceHeader({ - super.key, - required this.space, - required this.onAdded, - required this.onCreateNewSpace, - required this.onCollapseAllPages, - required this.isExpanded, - }); - - final ViewPB space; - final void Function(ViewLayoutPB layout) onAdded; - final VoidCallback onCreateNewSpace; - final VoidCallback onCollapseAllPages; - final bool isExpanded; - - @override - State createState() => _SidebarSpaceHeaderState(); -} - -class _SidebarSpaceHeaderState extends State { - final isHovered = ValueNotifier(false); - final onEditing = ValueNotifier(false); - - @override - void dispose() { - isHovered.dispose(); - onEditing.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: isHovered, - builder: (context, onHover, child) { - return MouseRegion( - onEnter: (_) => isHovered.value = true, - onExit: (_) => isHovered.value = false, - child: GestureDetector( - onTap: () => context - .read() - .add(SpaceEvent.expand(widget.space, !widget.isExpanded)), - child: _buildSpaceName(onHover), - ), - ); - }, - ); - } - - 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, - 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), - ), - ), - ), - Positioned( - right: 4, - child: _buildRightIcon(isHovered), - ), - ], - ), - ); - } - - 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) { - return ValueListenableBuilder( - valueListenable: onEditing, - builder: (context, onEditing, child) => Opacity( - opacity: isHovered || onEditing ? 1 : 0, - 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, - ), - ), - ], - ), - ), - ); - } - - Future _onAction(SpaceMoreActionType type, dynamic data) async { - switch (type) { - case SpaceMoreActionType.rename: - 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'); - } - } - } - break; - case SpaceMoreActionType.manage: - _showManageSpaceDialog(context); - break; - case SpaceMoreActionType.addNewSpace: - widget.onCreateNewSpace(); - break; - case SpaceMoreActionType.collapseAllPages: - widget.onCollapseAllPages(); - break; - case SpaceMoreActionType.delete: - _showDeleteSpaceDialog(context); - break; - case SpaceMoreActionType.duplicate: - context.read().add(const SpaceEvent.duplicate()); - break; - case SpaceMoreActionType.divider: - break; - } - } - - Future _showRenameDialog() async { - await NavigatorTextFieldDialog( - title: LocaleKeys.space_rename.tr(), - value: widget.space.name, - autoSelectAllText: true, - hintText: LocaleKeys.space_spaceName.tr(), - onConfirm: (name, _) { - context.read().add( - SpaceEvent.rename( - space: widget.space, - name: name, - ), - ); - }, - ).show(context); - } - - void _showManageSpaceDialog(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 ManageSpacePopup(), - ), - ); - }, - ); - } - - void _showDeleteSpaceDialog(BuildContext context) { - final spaceBloc = context.read(); - final space = spaceBloc.state.currentSpace; - final name = space != null ? space.name : ''; - showConfirmDeletionDialog( - context: context, - name: name, - description: LocaleKeys.space_deleteConfirmationDescription.tr(), - onConfirm: () { - context.read().add(const SpaceEvent.delete(null)); - }, - ); - } -} 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 deleted file mode 100644 index f4d910700d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.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/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: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 SidebarSpaceMenu extends StatelessWidget { - const SidebarSpaceMenu({ - super.key, - required this.showCreateButton, - }); - - final bool showCreateButton; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(4.0), - for (final space in state.spaces) - SizedBox( - height: HomeSpaceViewSizes.viewHeight, - child: SidebarSpaceMenuItem( - space: space, - isSelected: state.currentSpace?.id == space.id, - ), - ), - if (showCreateButton) ...[ - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: FlowyDivider(), - ), - const SizedBox( - height: HomeSpaceViewSizes.viewHeight, - child: _CreateSpaceButton(), - ), - ], - ], - ); - }, - ); - } -} - -class SidebarSpaceMenuItem extends StatelessWidget { - const SidebarSpaceMenuItem({ - super.key, - required this.space, - required this.isSelected, - }); - - final ViewPB space; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return FlowyButton( - text: Row( - children: [ - Flexible( - child: FlowyText.regular( - space.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(6.0), - if (space.spacePermission == SpacePermission.private) - FlowyTooltip( - message: LocaleKeys.space_privatePermissionDescription.tr(), - child: const FlowySvg( - FlowySvgs.space_lock_s, - ), - ), - ], - ), - iconPadding: 10, - leftIcon: SpaceIcon( - dimension: 20, - space: space, - svgSize: 12.0, - cornerRadius: 6.0, - ), - leftIconSize: const Size.square(20), - rightIcon: isSelected - ? const FlowySvg( - FlowySvgs.workspace_selected_s, - blendMode: null, - ) - : null, - onTap: () { - context.read().add(SpaceEvent.open(space)); - PopoverContainer.of(context).close(); - }, - ); - } -} - -class _CreateSpaceButton extends StatelessWidget { - const _CreateSpaceButton(); - - @override - Widget build(BuildContext context) { - return FlowyButton( - text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()), - iconPadding: 10, - leftIcon: const FlowySvg( - FlowySvgs.space_add_s, - ), - onTap: () { - PopoverContainer.of(context).close(); - _showCreateSpaceDialog(context); - }, - ); - } - - 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/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 deleted file mode 100644 index be0eadd8ed..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_action_type.dart +++ /dev/null @@ -1,73 +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:flutter/material.dart'; - -enum SpaceMoreActionType { - delete, - rename, - changeIcon, - collapseAllPages, - divider, - addNewSpace, - manage, - duplicate, -} - -extension ViewMoreActionTypeExtension on SpaceMoreActionType { - String get name { - switch (this) { - case SpaceMoreActionType.delete: - return LocaleKeys.space_delete.tr(); - case SpaceMoreActionType.rename: - return LocaleKeys.space_rename.tr(); - case SpaceMoreActionType.changeIcon: - return LocaleKeys.space_changeIcon.tr(); - case SpaceMoreActionType.collapseAllPages: - return LocaleKeys.space_collapseAllSubPages.tr(); - case SpaceMoreActionType.addNewSpace: - 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 { - switch (this) { - case SpaceMoreActionType.delete: - return FlowySvgs.trash_s; - case SpaceMoreActionType.rename: - return FlowySvgs.view_item_rename_s; - case SpaceMoreActionType.changeIcon: - return FlowySvgs.change_icon_s; - case SpaceMoreActionType.collapseAllPages: - return FlowySvgs.collapse_all_page_s; - case SpaceMoreActionType.addNewSpace: - return FlowySvgs.space_add_s; - case SpaceMoreActionType.manage: - return FlowySvgs.space_manage_s; - case SpaceMoreActionType.duplicate: - return FlowySvgs.duplicate_s; - case SpaceMoreActionType.divider: - throw UnsupportedError('Divider does not have an icon'); - } - } - - Widget get rightIcon { - switch (this) { - case SpaceMoreActionType.changeIcon: - case SpaceMoreActionType.rename: - case SpaceMoreActionType.collapseAllPages: - case SpaceMoreActionType.divider: - 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 deleted file mode 100644 index ad9e5e8f0a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon.dart +++ /dev/null @@ -1,154 +0,0 @@ -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, - ), - ), - ); - } -} 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 deleted file mode 100644 index 82410b387e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart +++ /dev/null @@ -1,390 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart' hide Icon; - -final builtInSpaceColors = [ - '0xFFA34AFD', - '0xFFFB006D', - '0xFF00C8FF', - '0xFFFFBA00', - '0xFFF254BC', - '0xFF2AC985', - '0xFFAAD93D', - '0xFF535CE4', - '0xFF808080', - '0xFFD2515F', - '0xFF409BF8', - '0xFFFF8933', -]; - -String generateRandomSpaceColor() { - final random = Random(); - return builtInSpaceColors[random.nextInt(builtInSpaceColors.length)]; -} - -final builtInSpaceIcons = - List.generate(15, (index) => 'space_icon_${index + 1}'); - -class SpaceIconPopup extends StatefulWidget { - const SpaceIconPopup({ - super.key, - 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 double cornerRadius; - - @override - State createState() => _SpaceIconPopupState(); -} - -class _SpaceIconPopupState extends State { - late ValueNotifier selectedIcon = ValueNotifier( - widget.icon, - ); - late ValueNotifier selectedColor = ValueNotifier( - widget.iconColor ?? builtInSpaceColors.first, - ); - - @override - void dispose() { - selectedColor.dispose(); - selectedIcon.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - offset: const Offset(0, 4), - constraints: BoxConstraints.loose(const Size(360, 432)), - margin: const EdgeInsets.all(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(); - }, - ); - }, - ); - } - - Widget _buildPreview() { - bool onHover = false; - return StatefulBuilder( - builder: (context, setState) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: ValueListenableBuilder( - valueListenable: selectedColor, - 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, - ), - ), - ), - ); - } - } - - if (onHover) { - return Stack( - children: [ - Positioned.fill( - child: Opacity(opacity: 0.2, child: child), - ), - const Center( - child: FlowySvg( - FlowySvgs.view_item_rename_s, - size: Size.square(20), - ), - ), - ], - ); - } - return child; - }, - ); - }, - ), - ); - }, - ); - } -} - -class SpaceIconPicker extends StatefulWidget { - const SpaceIconPicker({ - super.key, - required this.onIconChanged, - this.skipFirstNotification = false, - this.icon, - this.iconColor, - }); - - final bool skipFirstNotification; - final void Function(String icon, String color) onIconChanged; - final String? icon; - final String? iconColor; - - @override - State createState() => _SpaceIconPickerState(); -} - -class _SpaceIconPickerState extends State { - late ValueNotifier selectedColor = - ValueNotifier(widget.iconColor ?? builtInSpaceColors.first); - late ValueNotifier selectedIcon = - ValueNotifier(widget.icon ?? builtInSpaceIcons.first); - - @override - void initState() { - super.initState(); - - if (!widget.skipFirstNotification) { - widget.onIconChanged(selectedIcon.value, selectedColor.value); - } - - selectedColor.addListener(_onColorChanged); - selectedIcon.addListener(_onIconChanged); - } - - void _onColorChanged() { - widget.onIconChanged(selectedIcon.value, selectedColor.value); - } - - void _onIconChanged() { - widget.onIconChanged(selectedIcon.value, selectedColor.value); - } - - @override - void dispose() { - selectedColor.removeListener(_onColorChanged); - selectedColor.dispose(); - - selectedIcon.removeListener(_onIconChanged); - selectedIcon.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText.regular( - LocaleKeys.space_spaceIconBackground.tr(), - color: Theme.of(context).hintColor, - ), - const VSpace(10.0), - _Colors( - selectedColor: selectedColor.value, - onColorSelected: (color) => selectedColor.value = color, - ), - const VSpace(12.0), - FlowyText.regular( - LocaleKeys.space_spaceIcon.tr(), - color: Theme.of(context).hintColor, - ), - const VSpace(10.0), - ValueListenableBuilder( - valueListenable: selectedColor, - builder: (_, value, ___) => _Icons( - selectedColor: value, - selectedIcon: selectedIcon.value, - onIconSelected: (icon) => selectedIcon.value = icon, - ), - ), - ], - ); - } -} - -class _Colors extends StatefulWidget { - const _Colors({ - required this.selectedColor, - required this.onColorSelected, - }); - - final String selectedColor; - final void Function(String color) onColorSelected; - - @override - State<_Colors> createState() => _ColorsState(); -} - -class _ColorsState extends State<_Colors> { - late String selectedColor = widget.selectedColor; - - @override - Widget build(BuildContext context) { - return GridView.count( - shrinkWrap: true, - crossAxisCount: 6, - mainAxisSpacing: 4.0, - children: builtInSpaceColors.map((color) { - return GestureDetector( - onTap: () { - setState(() => selectedColor = color); - - widget.onColorSelected(color); - }, - child: Container( - margin: const EdgeInsets.all(2.0), - padding: const EdgeInsets.all(2.0), - decoration: selectedColor == color - ? ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide( - width: 1.50, - strokeAlign: BorderSide.strokeAlignOutside, - color: Color(0xFF00BCF0), - ), - borderRadius: BorderRadius.circular(20), - ), - ) - : null, - child: DecoratedBox( - decoration: BoxDecoration( - color: Color(int.parse(color)), - borderRadius: BorderRadius.circular(20.0), - ), - ), - ), - ); - }).toList(), - ); - } -} - -class _Icons extends StatefulWidget { - const _Icons({ - required this.selectedColor, - required this.selectedIcon, - required this.onIconSelected, - }); - - final String selectedColor; - final String selectedIcon; - final void Function(String color) onIconSelected; - - @override - State<_Icons> createState() => _IconsState(); -} - -class _IconsState extends State<_Icons> { - late String selectedIcon = widget.selectedIcon; - - @override - Widget build(BuildContext context) { - return GridView.count( - shrinkWrap: true, - crossAxisCount: 5, - mainAxisSpacing: 8.0, - crossAxisSpacing: 12.0, - children: builtInSpaceIcons.map((icon) { - return GestureDetector( - onTap: () { - setState(() => selectedIcon = icon); - - widget.onIconSelected(icon); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: FlowySvg( - FlowySvgData('assets/flowy_icons/16x/$icon.svg'), - color: Color(int.parse(widget.selectedColor)), - blendMode: BlendMode.srcOut, - ), - ), - ); - }).toList(), - ); - } -} 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 deleted file mode 100644 index 10ef94ba01..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_migration.dart +++ /dev/null @@ -1,160 +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: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_bloc/flutter_bloc.dart'; - -class SpaceMigration extends StatefulWidget { - const SpaceMigration({super.key}); - - @override - State createState() => _SpaceMigrationState(); -} - -class _SpaceMigrationState extends State { - bool _isExpanded = false; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(12), - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x66F5EAFF) - : const Color(0x1AFFFFFF), - shape: RoundedRectangleBorder( - side: const BorderSide( - strokeAlign: BorderSide.strokeAlignOutside, - color: Color(0x339327FF), - ), - borderRadius: BorderRadius.circular(10), - ), - ), - child: _isExpanded - ? _buildExpandedMigrationContent() - : _buildCollapsedMigrationContent(), - ); - } - - Widget _buildExpandedMigrationContent() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _MigrationTitle( - onClose: () => setState(() => _isExpanded = false), - ), - const VSpace(6.0), - Opacity( - opacity: 0.7, - child: FlowyText.regular( - LocaleKeys.space_upgradeSpaceDescription.tr(), - maxLines: null, - fontSize: 13.0, - lineHeight: 1.3, - ), - ), - const VSpace(12.0), - _ExpandedUpgradeButton( - onUpgrade: () => - context.read().add(const SpaceEvent.migrate()), - ), - ], - ); - } - - Widget _buildCollapsedMigrationContent() { - const linearGradient = LinearGradient( - begin: Alignment.bottomLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF8032FF), Color(0xFFEF35FF)], - stops: [0.1545, 0.8225], - ); - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => setState(() => _isExpanded = true), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.upgrade_s, - blendMode: null, - ), - const HSpace(8.0), - Expanded( - child: ShaderMask( - shaderCallback: (Rect bounds) => - linearGradient.createShader(bounds), - blendMode: BlendMode.srcIn, - child: FlowyText( - LocaleKeys.space_upgradeYourSpace.tr(), - ), - ), - ), - const FlowySvg( - FlowySvgs.space_arrow_right_s, - blendMode: null, - ), - ], - ), - ); - } -} - -class _MigrationTitle extends StatelessWidget { - const _MigrationTitle({required this.onClose}); - - final VoidCallback? onClose; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const FlowySvg( - FlowySvgs.upgrade_s, - blendMode: null, - ), - const HSpace(8.0), - Expanded( - child: FlowyText( - LocaleKeys.space_upgradeSpaceTitle.tr(), - maxLines: 3, - lineHeight: 1.2, - ), - ), - ], - ); - } -} - -class _ExpandedUpgradeButton extends StatelessWidget { - const _ExpandedUpgradeButton({required this.onUpgrade}); - - final VoidCallback? onUpgrade; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onUpgrade, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - decoration: ShapeDecoration( - color: const Color(0xFFA44AFD), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(9)), - ), - child: FlowyText( - LocaleKeys.space_upgrade.tr(), - color: Colors.white, - fontSize: 12.0, - strutStyle: const StrutStyle(forceStrutHeight: true), - ), - ), - ); - } -} 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 deleted file mode 100644 index 4b13062c3e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart +++ /dev/null @@ -1,211 +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/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/presentation/home/menu/sidebar/space/space_action_type.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: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 SpaceMorePopup extends StatelessWidget { - const SpaceMorePopup({ - super.key, - 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) { - final wrappers = _buildActionTypeWrappers(); - return PopoverActionList( - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 8), - actions: wrappers, - constraints: const BoxConstraints( - minWidth: 260, - ), - buildChild: (popover) { - return FlowyIconButton( - width: 24, - icon: FlowySvg( - FlowySvgs.workspace_three_dots_s, - color: isHovered ? Theme.of(context).colorScheme.onSurface : null, - ), - tooltipText: LocaleKeys.space_manage.tr(), - onPressed: () { - onEditing(true); - popover.show(); - }, - ); - }, - onSelected: (_, __) {}, - onClosed: () => onEditing(false), - ); - } - - List _buildActionTypeWrappers() { - final actionTypes = _buildActionTypes(); - return actionTypes - .map( - (e) => SpaceMoreActionTypeWrapper(e, (controller, data) { - onAction(e, data); - controller.close(); - }), - ) - .toList(); - } - - List _buildActionTypes() { - return [ - SpaceMoreActionType.rename, - SpaceMoreActionType.changeIcon, - SpaceMoreActionType.manage, - SpaceMoreActionType.duplicate, - SpaceMoreActionType.divider, - SpaceMoreActionType.addNewSpace, - SpaceMoreActionType.collapseAllPages, - SpaceMoreActionType.divider, - SpaceMoreActionType.delete, - ]; - } -} - -class SpaceMoreActionTypeWrapper extends CustomActionCell { - SpaceMoreActionTypeWrapper(this.inner, this.onTap); - - final SpaceMoreActionType inner; - final void Function(PopoverController controller, dynamic data) onTap; - - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - if (inner == SpaceMoreActionType.divider) { - return _buildDivider(); - } else if (inner == SpaceMoreActionType.changeIcon) { - return _buildEmojiActionButton(context, controller); - } else { - return _buildNormalActionButton(context, controller); - } - } - - Widget _buildNormalActionButton( - BuildContext context, - PopoverController controller, - ) { - return _buildActionButton(context, () => onTap(controller, null)); - } - - Widget _buildEmojiActionButton( - BuildContext context, - PopoverController controller, - ) { - final child = _buildActionButton(context, null); - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(360, 432)), - margin: const EdgeInsets.all(0), - clickHandler: PopoverClickHandler.gestureDetector, - offset: const Offset(0, -40), - popupBuilder: (context) { - return FlowyIconEmojiPicker( - tabs: const [PickerTabType.icon], - onSelectedEmoji: (r) => onTap(controller, r), - ); - }, - child: child, - ); - } - - Widget _buildDivider() { - return const Padding( - padding: EdgeInsets.all(8.0), - child: FlowyDivider(), - ); - } - - Widget _buildActionButton( - BuildContext context, - VoidCallback? onTap, - ) { - final spaceBloc = context.read(); - 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) { - disable = true; - message = LocaleKeys.space_unableToDeleteSpaceNotCreatedByYou.tr(); - } - } - - final child = Container( - height: 34, - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Opacity( - opacity: disable ? 0.3 : 1.0, - child: FlowyIconTextButton( - disable: disable, - margin: const EdgeInsets.symmetric(horizontal: 6), - iconPadding: 10.0, - 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, - ), - ), - ), - ); - - if (inner == SpaceMoreActionType.delete) { - return FlowyTooltip( - message: message, - child: child, - ); - } - - return child; - } -} 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 deleted file mode 100644 index 44f558fc17..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ /dev/null @@ -1,219 +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/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/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'; - -enum WorkspaceMoreAction { - rename, - delete, - leave, - divider, -} - -class WorkspaceMoreActionList extends StatefulWidget { - 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) { - final myRole = context.read().state.myRole; - final actions = []; - if (myRole.isOwner) { - actions.add(WorkspaceMoreAction.rename); - actions.add(WorkspaceMoreAction.divider); - actions.add(WorkspaceMoreAction.delete); - } else if (myRole.canLeave) { - actions.add(WorkspaceMoreAction.leave); - } - if (actions.isEmpty) { - return const SizedBox.shrink(); - } - return PopoverActionList<_WorkspaceMoreActionWrapper>( - direction: PopoverDirection.bottomWithLeftAligned, - actions: actions - .map( - (action) => _WorkspaceMoreActionWrapper( - action, - widget.workspace, - () => PopoverContainer.of(context).closeAll(), - ), - ) - .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, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 4.0), - text: const FlowySvg( - FlowySvgs.workspace_three_dots_s, - ), - onTap: () { - if (!isPopoverOpen) { - controller.show(); - isPopoverOpen = true; - } - }, - ), - ); - }, - onSelected: (action, controller) {}, - ); - } -} - -class _WorkspaceMoreActionWrapper extends CustomActionCell { - _WorkspaceMoreActionWrapper( - this.inner, - this.workspace, - this.closeWorkspaceMenu, - ); - - final WorkspaceMoreAction inner; - final UserWorkspacePB workspace; - final VoidCallback closeWorkspaceMenu; - - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - if (inner == WorkspaceMoreAction.divider) { - return const Divider(); - } - - return _buildActionButton(context, controller); - } - - Widget _buildActionButton( - BuildContext context, - PopoverController controller, - ) { - return FlowyIconTextButton( - leftIconBuilder: (onHover) => buildLeftIcon(context, onHover), - iconPadding: 10.0, - textBuilder: (onHover) => FlowyText.regular( - name, - fontSize: 14.0, - figmaLineHeight: 18.0, - color: [WorkspaceMoreAction.delete, WorkspaceMoreAction.leave] - .contains(inner) && - onHover - ? 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: () { - workspaceBloc.add( - UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), - ); - }, - ); - case WorkspaceMoreAction.rename: - await NavigatorTextFieldDialog( - title: LocaleKeys.workspace_renameWorkspace.tr(), - value: workspace.name, - hintText: '', - autoSelectAllText: true, - onConfirm: (name, context) async { - workspaceBloc.add( - UserWorkspaceEvent.renameWorkspace( - workspace.workspaceId, - name, - ), - ); - }, - ).show(context); - case WorkspaceMoreAction.leave: - await showConfirmDialog( - context: context, - title: LocaleKeys.workspace_leaveCurrentWorkspace.tr(), - description: - LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(), - confirmLabel: LocaleKeys.button_yes.tr(), - onConfirm: () { - workspaceBloc.add( - UserWorkspaceEvent.leaveWorkspace(workspace.workspaceId), - ); - }, - ); - } - }, - ); - } - - String get name { - switch (inner) { - case WorkspaceMoreAction.delete: - return LocaleKeys.button_delete.tr(); - case WorkspaceMoreAction.rename: - return LocaleKeys.button_rename.tr(); - case WorkspaceMoreAction.leave: - return LocaleKeys.workspace_leaveCurrentWorkspace.tr(); - case WorkspaceMoreAction.divider: - return ''; - } - } - - Widget buildLeftIcon(BuildContext context, bool onHover) { - switch (inner) { - case WorkspaceMoreAction.delete: - return FlowySvg( - FlowySvgs.trash_s, - color: onHover ? Theme.of(context).colorScheme.error : null, - ); - 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, - ); - 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 deleted file mode 100644 index 1f9f4b03b8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ /dev/null @@ -1,130 +0,0 @@ -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 '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: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({ - super.key, - required this.workspace, - required this.enableEdit, - required this.iconSize, - required this.fontSize, - required this.onSelected, - this.borderRadius = 4, - this.emojiSize, - this.alignment, - required this.figmaLineHeight, - this.showBorder = true, - }); - - final UserWorkspacePB workspace; - final double iconSize; - final bool enableEdit; - final double fontSize; - final double? emojiSize; - final void Function(EmojiIconData) onSelected; - final double borderRadius; - final Alignment? alignment; - final double figmaLineHeight; - final bool showBorder; - - @override - State createState() => _WorkspaceIconState(); -} - -class _WorkspaceIconState extends State { - final controller = PopoverController(); - - @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, - ) - : FlowyText.semibold( - widget.workspace.name.isEmpty - ? '' - : widget.workspace.name.substring(0, 1), - fontSize: widget.fontSize, - color: color.$1, - ); - - 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( - 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(); - }, - ), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), - ); - } - - 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, - ); - } -} 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 deleted file mode 100644 index 4ff5ccbf67..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ /dev/null @@ -1,527 +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/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'; -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-user/protobuf.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/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 { - const WorkspacesMenu({ - super.key, - required this.userProfile, - required this.currentWorkspace, - required this.workspaces, - }); - - final UserProfilePB userProfile; - final UserWorkspacePB currentWorkspace; - final List workspaces; - - @override - State createState() => _WorkspacesMenuState(); -} - -class _WorkspacesMenuState extends State { - final popoverMutex = PopoverMutex(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // user email - Padding( - padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0), - child: Row( - children: [ - Expanded( - child: FlowyText.medium( - _getUserInfo(), - fontSize: 12.0, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, - ), - ), - const HSpace(4.0), - WorkspaceMoreButton( - popoverMutex: popoverMutex, - ), - const HSpace(8.0), - ], - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.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(), - ), - ], - - const VSpace(6.0), - ], - ); - } - - String _getUserInfo() { - if (widget.userProfile.email.isNotEmpty) { - return widget.userProfile.email; - } - - if (widget.userProfile.name.isNotEmpty) { - return widget.userProfile.name; - } - - return LocaleKeys.defaultUsername.tr(); - } -} - -class WorkspaceMenuItem extends StatefulWidget { - const WorkspaceMenuItem({ - super.key, - 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(); -} - -class _WorkspaceMenuItemState extends State { - final ValueNotifier isHovered = ValueNotifier(false); - - @override - void dispose() { - isHovered.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => WorkspaceMemberBloc( - userProfile: widget.userProfile, - workspace: widget.workspace, - )..add(const WorkspaceMemberEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - // settings right icon inside the flowy button will - // cause the popover dismiss intermediately when click the right icon. - // so using the stack to put the right icon on the flowy button. - return SizedBox( - height: 44, - child: MouseRegion( - onEnter: (_) => isHovered.value = true, - onExit: (_) => isHovered.value = false, - child: Stack( - alignment: Alignment.center, - children: [ - _WorkspaceInfo( - isSelected: widget.isSelected, - workspace: widget.workspace, - ), - Positioned(left: 4, child: _buildLeftIcon(context)), - Positioned( - right: 4.0, - child: Align(child: _buildRightIcon(context, isHovered)), - ), - ], - ), - ), - ); - }, - ), - ); - } - - 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, - ), - ), - ), - ); - } - - Widget _buildRightIcon(BuildContext context, ValueNotifier isHovered) { - 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, - ), - ), - const HSpace(8.0), - if (widget.isSelected) ...[ - const Padding( - padding: EdgeInsets.all(5.0), - child: FlowySvg( - FlowySvgs.workspace_selected_s, - blendMode: null, - size: Size.square(14.0), - ), - ), - const HSpace(8.0), - ], - ], - ); - } -} - -class _WorkspaceInfo extends StatelessWidget { - const _WorkspaceInfo({ - required this.isSelected, - required this.workspace, - }); - - final bool isSelected; - final UserWorkspacePB workspace; - - @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, - ), - // 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(); - } - } -} - -class CreateWorkspaceDialog extends StatelessWidget { - const CreateWorkspaceDialog({ - super.key, - required this.onConfirm, - }); - - final void Function(String name) onConfirm; - - @override - Widget build(BuildContext context) { - return NavigatorTextFieldDialog( - title: LocaleKeys.workspace_create.tr(), - value: '', - hintText: '', - autoSelectAllText: true, - onConfirm: (name, _) => onConfirm(name), - ); - } -} - -class _CreateWorkspaceButton extends StatelessWidget { - const _CreateWorkspaceButton(); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 40, - child: FlowyButton( - key: createWorkspaceButtonKey, - onTap: () { - _showCreateWorkspaceDialog(context); - PopoverContainer.of(context).closeAll(); - }, - margin: const EdgeInsets.symmetric(horizontal: 4.0), - text: Row( - children: [ - _buildLeftIcon(context), - const HSpace(8.0), - FlowyText.regular( - LocaleKeys.workspace_create.tr(), - ), - ], - ), - ), - ); - } - - 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 _showCreateWorkspaceDialog(BuildContext context) async { - if (context.mounted) { - final workspaceBloc = context.read(); - await CreateWorkspaceDialog( - onConfirm: (name) { - workspaceBloc.add( - UserWorkspaceEvent.createWorkspace( - name, - AuthTypePB.Server, - ), - ); - }, - ).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; - - @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), - iconPadding: 10.0, - text: FlowyText.regular(LocaleKeys.button_logout.tr()), - onTap: () async { - await getIt().signOut(); - await runAppFlowy(); - }, - ), - child: SizedBox.square( - dimension: 24.0, - child: FlowyButton( - useIntrinsicWidth: true, - margin: EdgeInsets.zero, - text: const FlowySvg( - FlowySvgs.workspace_three_dots_s, - size: Size.square(16.0), - ), - onTap: () {}, - ), - ), - ); - } -} 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 deleted file mode 100644 index 50ea9d83c7..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ /dev/null @@ -1,321 +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/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/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: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 SidebarWorkspace extends StatefulWidget { - const SidebarWorkspace({super.key, required this.userProfile}); - - final UserProfilePB userProfile; - - @override - State createState() => _SidebarWorkspaceState(); -} - -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( - listenWhen: (previous, current) => - previous.actionResult != current.actionResult, - listener: _showResultDialog, - builder: (context, state) { - final currentWorkspace = state.currentWorkspace; - 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), - ], - ), - ); - }, - ), - ); - }, - ); - } - - void _showResultDialog(BuildContext context, UserWorkspaceState state) { - final actionResult = state.actionResult; - if (actionResult == null) { - return; - } - - 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', - ); - }); - - // show a confirmation dialog if the action is create and the result is LimitExceeded failure - if (actionType == UserWorkspaceActionType.create && - result.isFailure && - result.getFailure().code == ErrorCode.WorkspaceLimitExceeded) { - showDialog( - context: context, - builder: (context) => NavigatorOkCancelDialog( - message: LocaleKeys.workspace_createLimitExceeded.tr(), - ), - ); - return; - } - - final String? message; - switch (actionType) { - case UserWorkspaceActionType.create: - message = result.fold( - (s) => LocaleKeys.workspace_createSuccess.tr(), - (e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}', - ); - break; - case UserWorkspaceActionType.delete: - message = result.fold( - (s) => LocaleKeys.workspace_deleteSuccess.tr(), - (e) => '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}', - ); - break; - case UserWorkspaceActionType.open: - message = result.fold( - (s) => LocaleKeys.workspace_openSuccess.tr(), - (e) => '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}', - ); - break; - case UserWorkspaceActionType.updateIcon: - message = result.fold( - (s) => LocaleKeys.workspace_updateIconSuccess.tr(), - (e) => '${LocaleKeys.workspace_updateIconFailed.tr()}: ${e.msg}', - ); - break; - case UserWorkspaceActionType.rename: - message = result.fold( - (s) => LocaleKeys.workspace_renameSuccess.tr(), - (e) => '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}', - ); - break; - case UserWorkspaceActionType.none: - case UserWorkspaceActionType.fetchWorkspaces: - case UserWorkspaceActionType.leave: - message = null; - break; - } - - if (message != null) { - showToastNotification( - message: message, - type: result.fold( - (_) => ToastificationType.success, - (_) => ToastificationType.error, - ), - ); - } - } -} - -class SidebarSwitchWorkspaceButton extends StatefulWidget { - const SidebarSwitchWorkspaceButton({ - super.key, - required this.userProfile, - required this.currentWorkspace, - this.isHover = false, - }); - - final UserWorkspacePB currentWorkspace; - final UserProfilePB userProfile; - final bool isHover; - - @override - State createState() => - _SidebarSwitchWorkspaceButtonState(); -} - -class _SidebarSwitchWorkspaceButtonState - extends State { - final PopoverController _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - 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: () { - context - .read() - .add(const UserWorkspaceEvent.fetchWorkspaces()); - }, - onClose: () { - Log.info('close workspace menu'); - }, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: BlocBuilder( - builder: (context, state) { - final currentWorkspace = state.currentWorkspace; - final workspaces = state.workspaces; - if (currentWorkspace == null) { - return const SizedBox.shrink(); - } - Log.info('open workspace menu'); - return WorkspacesMenu( - userProfile: widget.userProfile, - currentWorkspace: currentWorkspace, - workspaces: workspaces, - ); - }, - ), - ); - }, - 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( - height: 30, - child: Row( - children: [ - const HSpace(4.0), - WorkspaceIcon( - workspace: currentWorkspace, - iconSize: 26, - fontSize: 16, - emojiSize: 20, - enableEdit: false, - borderRadius: 8.0, - figmaLineHeight: 18.0, - showBorder: false, - onSelected: (result) => context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - currentWorkspace.workspaceId, - result.emoji, - ), - ), - ), - const HSpace(6), - Flexible( - child: FlowyText.medium( - currentWorkspace.name, - color: - isHover ? Theme.of(context).colorScheme.onSurface : null, - 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, - ), - ], - ], - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index c604fae432..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:appflowy/workspace/application/menu/sidebar_sections_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/widgets/draggable_item/draggable_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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'; - -enum DraggableHoverPosition { - none, - top, - center, - bottom, -} - -const kDraggableViewItemDividerHeight = 2.0; - -class DraggableViewItem extends StatefulWidget { - const DraggableViewItem({ - super.key, - required this.view, - this.feedback, - required this.child, - this.isFirstChild = false, - this.centerHighlightColor, - this.topHighlightColor, - this.bottomHighlightColor, - this.onDragging, - this.onMove, - }); - - final Widget child; - final WidgetBuilder? feedback; - final ViewPB view; - final bool isFirstChild; - final Color? centerHighlightColor; - final Color? topHighlightColor; - final Color? bottomHighlightColor; - final void Function(bool isDragging)? onDragging; - final void Function(ViewPB from, ViewPB to)? onMove; - - @override - State createState() => _DraggableViewItemState(); -} - -class _DraggableViewItemState extends State { - DraggableHoverPosition position = DraggableHoverPosition.none; - final hoverColor = const Color(0xFF00C8FF); - - @override - Widget build(BuildContext context) { - // 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 - ? _buildMobileDraggableItem() - : _buildDesktopDraggableItem(); - - return DraggableItem( - data: widget.view, - onDragging: widget.onDragging, - onWillAcceptWithDetails: (data) => true, - 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; - } - _updatePosition(position); - }, - onLeave: (_) => _updatePosition( - DraggableHoverPosition.none, - ), - onAcceptWithDetails: (details) { - final data = details.data; - _move(data, widget.view); - _updatePosition(DraggableHoverPosition.none); - }, - feedback: IntrinsicWidth( - child: Opacity( - opacity: 0.5, - child: widget.feedback?.call(context) ?? child, - ), - ), - child: child, - ); - } - - Widget _buildDesktopDraggableItem() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // only show the top border when the draggable item is the first child - if (widget.isFirstChild) - Divider( - height: kDraggableViewItemDividerHeight, - thickness: kDraggableViewItemDividerHeight, - color: position == DraggableHoverPosition.top - ? widget.topHighlightColor ?? hoverColor - : Colors.transparent, - ), - DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - color: position == DraggableHoverPosition.center - ? widget.centerHighlightColor ?? - hoverColor.withValues(alpha: 0.5) - : Colors.transparent, - ), - child: widget.child, - ), - Divider( - height: kDraggableViewItemDividerHeight, - thickness: kDraggableViewItemDividerHeight, - color: position == DraggableHoverPosition.bottom - ? widget.bottomHighlightColor ?? hoverColor - : Colors.transparent, - ), - ], - ); - } - - Widget _buildMobileDraggableItem() { - return Stack( - children: [ - if (widget.isFirstChild) - Positioned( - top: 0, - left: 0, - right: 0, - height: kDraggableViewItemDividerHeight, - child: Divider( - height: kDraggableViewItemDividerHeight, - thickness: kDraggableViewItemDividerHeight, - color: position == DraggableHoverPosition.top - ? widget.topHighlightColor ?? - Theme.of(context).colorScheme.secondary - : Colors.transparent, - ), - ), - DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - color: position == DraggableHoverPosition.center - ? widget.centerHighlightColor ?? - Theme.of(context) - .colorScheme - .secondary - .withValues(alpha: 0.5) - : Colors.transparent, - ), - child: widget.child, - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - height: kDraggableViewItemDividerHeight, - child: Divider( - height: kDraggableViewItemDividerHeight, - thickness: kDraggableViewItemDividerHeight, - color: position == DraggableHoverPosition.bottom - ? widget.bottomHighlightColor ?? - Theme.of(context).colorScheme.secondary - : Colors.transparent, - ), - ), - ], - ); - } - - void _updatePosition(DraggableHoverPosition position) { - if (UniversalPlatform.isMobile && position != this.position) { - HapticFeedback.mediumImpact(); - } - setState(() => this.position = position); - } - - void _move(ViewPB from, ViewPB to) { - if (position == DraggableHoverPosition.center && - to.layout != ViewLayoutPB.Document) { - // not support moving into a database - return; - } - - if (widget.onMove != null) { - widget.onMove?.call(from, to); - return; - } - - final fromSection = getViewSection(from); - final toSection = getViewSection(to); - - switch (position) { - case DraggableHoverPosition.top: - context.read().add( - ViewEvent.move( - from, - to.parentViewId, - null, - fromSection, - toSection, - ), - ); - break; - case DraggableHoverPosition.bottom: - context.read().add( - ViewEvent.move( - from, - to.parentViewId, - to.id, - fromSection, - toSection, - ), - ); - break; - case DraggableHoverPosition.center: - context.read().add( - ViewEvent.move( - from, - to.id, - to.childViews.lastOrNull?.id, - fromSection, - toSection, - ), - ); - break; - case DraggableHoverPosition.none: - break; - } - } - - DraggableHoverPosition _computeHoverPosition(Offset offset, Size size) { - final threshold = size.height / 5.0; - if (widget.isFirstChild && offset.dy < -5.0) { - return DraggableHoverPosition.top; - } - if (offset.dy > threshold) { - return DraggableHoverPosition.bottom; - } - return DraggableHoverPosition.center; - } - - bool _shouldAccept(ViewPB data, DraggableHoverPosition position) { - // could not move the view to a database - if (widget.view.layout.isDatabaseView && - position == DraggableHoverPosition.center) { - return false; - } - - // ignore moving the view to itself - if (data.id == widget.view.id) { - return false; - } - - // ignore moving the view to its child view - if (data.containsView(widget.view)) { - return false; - } - - return true; - } - - ViewSectionPB? getViewSection(ViewPB view) { - return context.read().getViewSection(view); - } -} - -extension on ViewPB { - bool containsView(ViewPB view) { - if (id == view.id) { - return true; - } - - return childViews.any((v) => v.containsView(view)); - } -} 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 deleted file mode 100644 index d4f91b67d9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart +++ /dev/null @@ -1,111 +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:flutter/material.dart'; - -enum ViewMoreActionType { - delete, - favorite, - unFavorite, - duplicate, - copyLink, // not supported yet. - rename, - moveTo, - openInNewTab, - changeIcon, - collapseAllPages, // including sub pages - divider, - lastModified, - created, - lockPage; - - static const disableInLockedView = [ - delete, - rename, - moveTo, - changeIcon, - ]; -} - -extension ViewMoreActionTypeExtension on ViewMoreActionType { - String get name { - switch (this) { - case ViewMoreActionType.delete: - return LocaleKeys.disclosureAction_delete.tr(); - case ViewMoreActionType.favorite: - return LocaleKeys.disclosureAction_favorite.tr(); - case ViewMoreActionType.unFavorite: - return LocaleKeys.disclosureAction_unfavorite.tr(); - case ViewMoreActionType.duplicate: - return LocaleKeys.disclosureAction_duplicate.tr(); - case ViewMoreActionType.copyLink: - return LocaleKeys.disclosureAction_copyLink.tr(); - case ViewMoreActionType.rename: - return LocaleKeys.disclosureAction_rename.tr(); - case ViewMoreActionType.moveTo: - return LocaleKeys.disclosureAction_moveTo.tr(); - case ViewMoreActionType.openInNewTab: - return LocaleKeys.disclosureAction_openNewTab.tr(); - case ViewMoreActionType.changeIcon: - 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: - return ''; - } - } - - FlowySvgData get leftIconSvg { - switch (this) { - case ViewMoreActionType.delete: - return FlowySvgs.trash_s; - case ViewMoreActionType.favorite: - return FlowySvgs.favorite_s; - case ViewMoreActionType.unFavorite: - return FlowySvgs.unfavorite_s; - case ViewMoreActionType.duplicate: - return FlowySvgs.duplicate_s; - case ViewMoreActionType.rename: - return FlowySvgs.view_item_rename_s; - case ViewMoreActionType.moveTo: - return FlowySvgs.move_to_s; - case ViewMoreActionType.openInNewTab: - return FlowySvgs.view_item_open_in_new_tab_s; - case ViewMoreActionType.changeIcon: - return FlowySvgs.change_icon_s; - case ViewMoreActionType.collapseAllPages: - return FlowySvgs.collapse_all_page_s; - case ViewMoreActionType.lockPage: - return FlowySvgs.lock_page_s; - case ViewMoreActionType.divider: - case ViewMoreActionType.lastModified: - case ViewMoreActionType.copyLink: - case ViewMoreActionType.created: - throw UnsupportedError('No left icon for $this'); - } - } - - Widget get rightIcon { - switch (this) { - case ViewMoreActionType.changeIcon: - case ViewMoreActionType.moveTo: - case ViewMoreActionType.favorite: - case ViewMoreActionType.unFavorite: - case ViewMoreActionType.duplicate: - case ViewMoreActionType.copyLink: - case ViewMoreActionType.rename: - case ViewMoreActionType.openInNewTab: - case ViewMoreActionType.collapseAllPages: - case ViewMoreActionType.divider: - 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 deleted file mode 100644 index d0b99e2a5d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/document.dart'; -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class ViewAddButton extends StatelessWidget { - const ViewAddButton({ - super.key, - required this.parentViewId, - required this.onEditing, - required this.onSelected, - this.isHovered = false, - }); - - final String parentViewId; - final void Function(bool value) onEditing; - final Function( - PluginBuilder, - String? name, - List? initialDataBytes, - bool openAfterCreated, - bool createNewView, - ) onSelected; - final bool isHovered; - - List get _actions { - return [ - // document, grid, kanban, calendar - ...pluginBuilders().map( - (pluginBuilder) => ViewAddButtonActionWrapper( - pluginBuilder: pluginBuilder, - ), - ), - // import from ... - ...getIt().builders.whereType().map( - (pluginBuilder) => ViewImportActionWrapper( - pluginBuilder: pluginBuilder, - ), - ), - ]; - } - - @override - Widget build(BuildContext context) { - return PopoverActionList( - direction: PopoverDirection.bottomWithLeftAligned, - actions: _actions, - offset: const Offset(0, 8), - constraints: const BoxConstraints( - minWidth: 200, - ), - buildChild: (popover) { - return FlowyIconButton( - width: 24, - icon: FlowySvg( - FlowySvgs.view_item_add_s, - color: isHovered ? Theme.of(context).colorScheme.onSurface : null, - ), - onPressed: () { - onEditing(true); - popover.show(); - }, - ); - }, - onSelected: (action, popover) { - onEditing(false); - if (action is ViewAddButtonActionWrapper) { - _showViewAddButtonActions(context, action); - } else if (action is ViewImportActionWrapper) { - _showViewImportAction(context, action); - } - popover.close(); - }, - onClosed: () { - onEditing(false); - }, - ); - } - - void _showViewAddButtonActions( - BuildContext context, - ViewAddButtonActionWrapper action, - ) { - onSelected(action.pluginBuilder, null, null, true, true); - } - - void _showViewImportAction( - BuildContext context, - ViewImportActionWrapper action, - ) { - showImportPanel( - parentViewId, - context, - (type, name, initialDataBytes) { - onSelected(action.pluginBuilder, null, null, true, false); - }, - ); - } -} - -class ViewAddButtonActionWrapper extends ActionCell { - ViewAddButtonActionWrapper({ - required this.pluginBuilder, - }); - - final PluginBuilder pluginBuilder; - - @override - Widget? leftIcon(Color iconColor) => FlowySvg( - pluginBuilder.icon, - size: const Size.square(16), - ); - - @override - String get name => pluginBuilder.menuName; - - PluginType get pluginType => pluginBuilder.pluginType; -} - -class ViewImportActionWrapper extends ActionCell { - ViewImportActionWrapper({ - required this.pluginBuilder, - }); - - final DocumentPluginBuilder pluginBuilder; - - @override - Widget? leftIcon(Color iconColor) => const FlowySvg(FlowySvgs.icon_import_s); - - @override - String get name => LocaleKeys.moreAction_import.tr(); -} 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 deleted file mode 100644 index 22182f7429..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ /dev/null @@ -1,944 +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/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/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/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: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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -typedef ViewItemOnSelected = void Function(BuildContext context, ViewPB view); -typedef ViewItemLeftIconBuilder = Widget Function( - BuildContext context, - ViewPB view, -); -typedef ViewItemRightIconsBuilder = List Function( - BuildContext context, - ViewPB view, -); - -enum IgnoreViewType { none, hide, disable } - -class ViewItem extends StatelessWidget { - const ViewItem({ - super.key, - required this.view, - this.parentView, - required this.spaceType, - required this.level, - this.leftPadding = 10, - required this.onSelected, - this.onTertiarySelected, - this.isFirstChild = false, - this.isDraggable = true, - required this.isFeedback, - this.height = HomeSpaceViewSizes.viewHeight, - this.isHoverEnabled = false, - this.isPlaceholder = false, - this.isHovered, - this.shouldRenderChildren = true, - this.leftIconBuilder, - this.rightIconsBuilder, - this.shouldLoadChildViews = true, - this.isExpandedNotifier, - this.extendBuilder, - this.disableSelectedStatus, - this.shouldIgnoreView, - this.engagedInExpanding = false, - this.enableRightClickContext = false, - }); - - final ViewPB view; - final ViewPB? parentView; - - final FolderSpaceType spaceType; - - // indicate the level of the view item - // used to calculate the left padding - final int level; - - // the left padding of the view item for each level - // the left padding of the each level = level * leftPadding - final double leftPadding; - - // Selected by normal conventions - final ViewItemOnSelected onSelected; - - // Selected by middle mouse button - final ViewItemOnSelected? onTertiarySelected; - - // used for indicating the first child of the parent view, so that we can - // add top border to the first child - final bool isFirstChild; - - // it should be false when it's rendered as feedback widget inside DraggableItem - final bool isDraggable; - - // identify if the view item is rendered as feedback widget inside DraggableItem - final bool isFeedback; - - final double height; - - final bool isHoverEnabled; - - // all the view movement depends on the [ViewItem] widget, so we have to add a - // placeholder widget to receive the drop event when moving view across sections. - final bool isPlaceholder; - - // used for control the expand/collapse icon - final ValueNotifier? isHovered; - - // render the child views of the view - final bool shouldRenderChildren; - - // 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; - - final bool shouldLoadChildViews; - final PropertyValueNotifier? isExpandedNotifier; - - 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()), - child: BlocConsumer( - listenWhen: (p, c) => - c.lastCreatedView != null && - p.lastCreatedView?.id != c.lastCreatedView!.id, - 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( - view: state.view, - parentView: parentView, - childViews: childViews, - spaceType: spaceType, - level: level, - leftPadding: leftPadding, - showActions: state.isEditing, - enableRightClickContext: enableRightClickContext, - isExpanded: state.isExpanded, - disableSelectedStatus: disableSelectedStatus, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - isFirstChild: isFirstChild, - isDraggable: isDraggable, - isFeedback: isFeedback, - height: height, - isHoverEnabled: isHoverEnabled, - isPlaceholder: isPlaceholder, - isHovered: isHovered, - shouldRenderChildren: shouldRenderChildren, - leftIconBuilder: leftIconBuilder, - 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 { - const InnerViewItem({ - super.key, - required this.view, - required this.parentView, - required this.childViews, - required this.spaceType, - this.isDraggable = true, - this.isExpanded = true, - required this.level, - required this.leftPadding, - required this.showActions, - this.enableRightClickContext = false, - required this.onSelected, - this.onTertiarySelected, - this.isFirstChild = false, - required this.isFeedback, - required this.height, - this.isHoverEnabled = true, - this.isPlaceholder = false, - this.isHovered, - this.shouldRenderChildren = true, - required this.leftIconBuilder, - required this.rightIconsBuilder, - this.isExpandedNotifier, - required this.extendBuilder, - this.disableSelectedStatus, - this.engagedInExpanding = false, - required this.shouldIgnoreView, - }); - - final ViewPB view; - final ViewPB? parentView; - final List childViews; - final FolderSpaceType spaceType; - - final bool isDraggable; - final bool isExpanded; - final bool isFirstChild; - - // identify if the view item is rendered as feedback widget inside DraggableItem - final bool isFeedback; - - final int level; - 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; - final ViewItemRightIconsBuilder? rightIconsBuilder; - - final PropertyValueNotifier? isExpandedNotifier; - final List Function(ViewPB view)? extendBuilder; - final IgnoreViewType Function(ViewPB view)? shouldIgnoreView; - final bool engagedInExpanding; - - @override - State createState() => _InnerViewItemState(); -} - -class _InnerViewItemState extends State { - @override - void initState() { - super.initState(); - widget.isExpandedNotifier?.addListener(_collapseAllPages); - } - - @override - void dispose() { - widget.isExpandedNotifier?.removeListener(_collapseAllPages); - super.dispose(); - } - - @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, - ); - }, - ); - - // if the view is expanded and has child views, render its child views - if (widget.isExpanded && - widget.shouldRenderChildren && - widget.childViews.isNotEmpty) { - final children = widget.childViews.map((childView) { - return ViewItem( - key: ValueKey('${widget.spaceType.name} ${childView.id}'), - parentView: widget.view, - spaceType: widget.spaceType, - 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, - isHovered: widget.isHovered, - leftIconBuilder: widget.leftIconBuilder, - rightIconsBuilder: widget.rightIconsBuilder, - extendBuilder: widget.extendBuilder, - shouldIgnoreView: widget.shouldIgnoreView, - engagedInExpanding: widget.engagedInExpanding, - ); - }).toList(); - - child = Column( - mainAxisSize: MainAxisSize.min, - children: [child, ...children], - ); - } - - // wrap the child with DraggableItem if isDraggable is true - if ((widget.isDraggable || widget.isPlaceholder) && - !isReferencedDatabaseView(widget.view, widget.parentView)) { - child = DraggableViewItem( - isFirstChild: widget.isFirstChild, - view: widget.view, - onDragging: (isDragging) => _isDragging = isDragging, - onMove: widget.isPlaceholder - ? (from, to) => moveViewCrossSpace( - context, - null, - widget.view, - widget.parentView, - widget.spaceType, - from, - to.parentViewId, - ) - : 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, - ), - ), - child: child, - ); - } else { - // keep the same height of the DraggableItem - child = Padding( - padding: const EdgeInsets.only(top: kDraggableViewItemDividerHeight), - child: child, - ); - } - - return child; - } - - void _collapseAllPages() { - if (widget.isExpandedNotifier?.value == true) { - context.read().add(const ViewEvent.collapseAllPages()); - } - } -} - -class SingleInnerViewItem extends StatefulWidget { - const SingleInnerViewItem({ - super.key, - required this.view, - required this.parentView, - required this.isExpanded, - required this.level, - required this.leftPadding, - this.isDraggable = true, - required this.spaceType, - required this.showActions, - this.enableRightClickContext = false, - required this.onSelected, - this.onTertiarySelected, - required this.isFeedback, - required this.height, - this.isHoverEnabled = true, - this.isPlaceholder = false, - this.isHovered, - 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; - - final int level; - final double leftPadding; - - final bool isDraggable; - final bool showActions; - final bool enableRightClickContext; - final ViewItemOnSelected onSelected; - final ViewItemOnSelected? onTertiarySelected; - final FolderSpaceType spaceType; - final double height; - - 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(); -} - -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; - } - - if (widget.isPlaceholder) { - return const SizedBox(height: 4, width: double.infinity); - } - - if (widget.isFeedback || !widget.isHoverEnabled) { - return _buildViewItem( - false, - !widget.isHoverEnabled ? isSelected : false, - ); - } - - return FlowyHover( - 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), - ); - } - - Widget _buildViewItem(bool onHover, [bool isSelected = false]) { - final name = FlowyText.regular( - widget.view.nameOrDefault, - overflow: TextOverflow.ellipsis, - fontSize: 14.0, - figmaLineHeight: 18.0, - ); - final children = [ - const HSpace(2), - // expand icon or placeholder - widget.leftIconBuilder?.call(context, widget.view) ?? _buildLeftIcon(), - const HSpace(2), - // icon - _buildViewIconButton(), - const HSpace(6), - // title - Expanded( - child: widget.extendBuilder != null - ? Row( - children: [ - Flexible(child: name), - ...widget.extendBuilder!(widget.view), - ], - ) - : name, - ), - ]; - - // hover action - if (widget.showActions || onHover) { - if (widget.rightIconsBuilder != null) { - 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, - ), - ), - ), - ); - // only support add button for document layout - if (widget.view.layout == ViewLayoutPB.Document) { - // + button - children.add(const HSpace(8.0)); - children.add(_buildViewAddButton(context)); - } - children.add(const HSpace(4.0)); - } - } - - final child = GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => widget.onSelected(context, widget.view), - onTertiaryTapDown: (_) => - widget.onTertiarySelected?.call(context, widget.view), - child: SizedBox( - 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), - ), - ), - ), - ); - - if (isSelected) { - final popoverController = getIt().state.controller; - return AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - offset: const Offset(0, 5), - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (_) => RenameViewPopover( - view: widget.view, - name: widget.view.name, - emoji: widget.view.icon.toEmojiIconData(), - popoverController: popoverController, - showIconChanger: false, - ), - child: child, - ); - } - - return child; - } - - Widget _buildViewIconButton() { - final iconData = widget.view.icon.toEmojiIconData(); - final icon = iconData.isNotEmpty - ? RawEmojiIconWidget( - emoji: iconData, - emojiSize: 16.0, - lineHeight: 18.0 / 16.0, - ) - : Opacity(opacity: 0.6, child: widget.view.defaultIcon()); - - final Widget child = 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 - onTap: () {}, - child: FlowyTooltip( - message: LocaleKeys.document_plugins_cover_changeIcon.tr(), - child: SizedBox(width: 16.0, child: icon), - ), - ), - popupBuilder: (context) { - isIconPickerOpened = true; - return FlowyIconEmojiPicker( - initialType: iconData.type.toPickerTabType(), - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], - documentId: widget.view.id, - onSelectedEmoji: (r) { - ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: r.data, - ); - if (!r.keepOpen) controller.close(); - }, - ); - }, - ); - - if (widget.view.isLocked) { - return LockPageButtonWrapper( - child: child, - ); - } - - 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) { - return FlowyTooltip( - message: LocaleKeys.menuAppHeader_addPageTooltip.tr(), - child: ViewAddButton( - parentViewId: widget.view.id, - onEditing: (value) => - context.read().add(ViewEvent.setIsEditing(value)), - onSelected: _onSelected, - ), - ); - } - - 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( - 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 { - switch (action) { - case ViewMoreActionType.favorite: - case ViewMoreActionType.unFavorite: - context - .read() - .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), - ); - 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()); - } - break; - case ViewMoreActionType.duplicate: - context.read().add(const ViewEvent.duplicate()); - break; - case ViewMoreActionType.openInNewTab: - context.read().openTab(widget.view); - break; - case ViewMoreActionType.collapseAllPages: - context.read().add(const ViewEvent.collapseAllPages()); - break; - case ViewMoreActionType.changeIcon: - if (data is! SelectedEmojiIconResult) { - return; - } - await ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: data.data, - ); - 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'); - } - }, - ), - ); - } -} - -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), - ), - ), - ); - } -} - -// workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field. -bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) { - if (parentView == null) { - return false; - } - 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 deleted file mode 100644 index 5b531c2f28..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ /dev/null @@ -1,313 +0,0 @@ -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/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: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({ - 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, - 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(); - } - - List _buildActionTypes() { - final List actionTypes = []; - - if (spaceType == FolderSpaceType.favorite) { - actionTypes.addAll([ - ViewMoreActionType.unFavorite, - ViewMoreActionType.divider, - ViewMoreActionType.rename, - ViewMoreActionType.openInNewTab, - ]); - } else { - actionTypes.add( - view.isFavorite - ? ViewMoreActionType.unFavorite - : ViewMoreActionType.favorite, - ); - - actionTypes.addAll([ - ViewMoreActionType.divider, - ViewMoreActionType.rename, - ]); - - // Chat doesn't change icon and duplicate - if (view.layout != ViewLayoutPB.Chat) { - actionTypes.addAll([ - ViewMoreActionType.changeIcon, - ViewMoreActionType.duplicate, - ]); - } - - 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) { - actionTypes.add(ViewMoreActionType.collapseAllPages); - actionTypes.add(ViewMoreActionType.divider); - } - - actionTypes.add(ViewMoreActionType.openInNewTab); - } - - return actionTypes; - } -} - -class ViewMoreActionTypeWrapper extends CustomActionCell { - ViewMoreActionTypeWrapper( - this.inner, - this.sourceView, - this.onTap, { - this.moveActionDirection, - this.moveActionOffset, - }); - - 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; - - if (inner == ViewMoreActionType.divider) { - child = _buildDivider(); - } else if (inner == ViewMoreActionType.lastModified) { - child = _buildLastModified(context); - } else if (inner == ViewMoreActionType.created) { - child = _buildCreated(context); - } else if (inner == ViewMoreActionType.changeIcon) { - child = _buildEmojiActionButton(context, controller); - } else if (inner == ViewMoreActionType.moveTo) { - child = _buildMoveToActionButton(context, controller); - } else { - child = _buildNormalActionButton(context, controller); - } - - if (ViewMoreActionType.disableInLockedView.contains(inner) && - sourceView.isLocked) { - child = LockPageButtonWrapper( - child: child, - ); - } - - return child; - } - - Widget _buildNormalActionButton( - BuildContext context, - PopoverController controller, - ) { - return _buildActionButton(context, () => onTap(controller, null)); - } - - Widget _buildEmojiActionButton( - BuildContext context, - PopoverController controller, - ) { - final child = _buildActionButton(context, null); - - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(364, 356)), - margin: const EdgeInsets.all(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), - ), - 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(), - ); - } - - Widget _buildLastModified(BuildContext context) { - return Container( - height: 40, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - ), - ); - } - - Widget _buildCreated(BuildContext context) { - return Container( - height: 40, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - ), - ); - } - - Widget _buildActionButton( - BuildContext context, - VoidCallback? onTap, - ) { - return Container( - height: 34, - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyIconTextButton( - 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, - 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, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart index d588e512b0..f5cacd3cfd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart @@ -1,41 +1,45 @@ import 'dart:io'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.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/flowy_tooltip.dart'; 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'; + +typedef NaviAction = void Function(); class NavigationNotifier with ChangeNotifier { + List navigationItems; NavigationNotifier({required this.navigationItems}); - List navigationItems; - - void update(PageNotifier notifier) { + void update(HomeStackNotifier notifier) { + bool shouldNotify = false; if (navigationItems != notifier.plugin.widgetBuilder.navigationItems) { navigationItems = notifier.plugin.widgetBuilder.navigationItems; + shouldNotify = true; + } + + if (shouldNotify) { notifyListeners(); } } } class FlowyNavigation extends StatelessWidget { - const FlowyNavigation({super.key}); + const FlowyNavigation({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return ChangeNotifierProxyProvider( + return ChangeNotifierProxyProvider( create: (_) { - final notifier = Provider.of(context, listen: false); + final notifier = Provider.of(context, listen: false); return NavigationNotifier( navigationItems: notifier.plugin.widgetBuilder.navigationItems, ); @@ -50,6 +54,7 @@ class FlowyNavigation extends StatelessWidget { builder: (ctx, items, child) => Expanded( child: Row( children: _renderNavigationItems(items), + // crossAxisAlignment: WrapCrossAlignment.start, ), ), ), @@ -63,44 +68,33 @@ class FlowyNavigation extends StatelessWidget { return BlocBuilder( buildWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed, builder: (context, state) { - if (!UniversalPlatform.isWindows && state.isMenuCollapsed) { - final textSpan = TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.sideBar_openSidebar.tr()}\n', - style: context.tooltipTextStyle(), + if (state.isMenuCollapsed) { + return RotationTransition( + turns: const AlwaysStoppedAnimation(180 / 360), + child: Tooltip( + richMessage: sidebarTooltipTextSpan( + context, + LocaleKeys.sideBar_openSidebar.tr(), ), - TextSpan( - text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), - ), - ], - ); - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: RotationTransition( - turns: const AlwaysStoppedAnimation(180 / 360), - child: FlowyTooltip( - richMessage: textSpan, - child: Listener( - onPointerDown: (event) => context + child: FlowyIconButton( + width: 24, + hoverColor: Colors.transparent, + onPressed: () { + context .read() - .add(const HomeSettingEvent.collapseMenu()), - child: FlowyIconButton( - width: 24, - onPressed: () {}, - iconPadding: const EdgeInsets.all(4), - icon: const FlowySvg(FlowySvgs.hide_menu_s), - ), + .add(const HomeSettingEvent.collapseMenu()); + }, + iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), + icon: svgWidget( + "home/hide_menu", + color: Theme.of(context).iconTheme.color, ), ), ), ); + } else { + return Container(); } - - return const SizedBox.shrink(); }, ); } @@ -144,9 +138,8 @@ class FlowyNavigation extends StatelessWidget { } class NaviItemWidget extends StatelessWidget { - const NaviItemWidget(this.item, {super.key}); - final NavigationItem item; + const NaviItemWidget(this.item, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -156,19 +149,29 @@ class NaviItemWidget extends StatelessWidget { } } +class NaviItemDivider extends StatelessWidget { + final Widget child; + const NaviItemDivider({Key? key, required this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [child, const Text('/')], + ); + } +} + class EllipsisNaviItem extends NavigationItem { - EllipsisNaviItem({required this.items}); - final List items; + EllipsisNaviItem({ + required this.items, + }); @override - String? get viewName => null; - - @override - Widget get leftBarItem => FlowyText.medium('...', fontSize: FontSizes.s16); - - @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => leftBarItem; + Widget get leftBarItem => FlowyText.medium( + '...', + fontSize: FontSizes.s16, + ); @override NavigationCallback get action => (id) {}; @@ -181,7 +184,7 @@ TextSpan sidebarTooltipTextSpan(BuildContext context, String hintText) => text: "$hintText\n", ), TextSpan( - text: Platform.isMacOS ? "⌘+." : "Ctrl+\\", + text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", ), ], ); 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 deleted file mode 100644 index 7e4a5f8df1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart +++ /dev/null @@ -1,237 +0,0 @@ -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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; - -class FlowyTab extends StatefulWidget { - const FlowyTab({ - 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(); - - @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, - ), - 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), - ), - ), - ), - ), - ], - ], - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ); - } - - void _closeTab(BuildContext context) => 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 deleted file mode 100644 index 38ede2421e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart +++ /dev/null @@ -1,62 +0,0 @@ -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_bloc/flutter_bloc.dart'; - -class TabsManager extends StatelessWidget { - const TabsManager({super.key, required this.onIndexChanged}); - - final void Function(int) onIndexChanged; - - @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(); - } - - final isAllPinned = state.isAllPinned; - - 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(), - ), - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 22906ce724..995f8afbc2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -1,20 +1,16 @@ -import 'package:appflowy/generated/locale_keys.g.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/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}); - final String message; + const FlowyMessageToast({required this.message, Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return DecoratedBox( + return Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4)), color: Theme.of(context).colorScheme.surface, @@ -24,7 +20,6 @@ class FlowyMessageToast extends StatelessWidget { child: FlowyText.medium( message, fontSize: FontSizes.s16, - maxLines: 3, ), ), ); @@ -35,43 +30,22 @@ void initToastWithContext(BuildContext context) { getIt().init(context); } -void showMessageToast( - String message, { - BuildContext? context, - ToastGravity gravity = ToastGravity.BOTTOM, -}) { +void showMessageToast(String message) { final child = FlowyMessageToast(message: message); - final toast = context == null ? getIt() : (FToast()..init(context)); - toast.showToast( + + getIt().showToast( child: child, - gravity: gravity, + gravity: ToastGravity.BOTTOM, toastDuration: const Duration(seconds: 3), ); } -void showSnackBarMessage( - BuildContext context, - String message, { - bool showCancel = false, - Duration duration = const Duration(seconds: 4), -}) { +void showSnackBarMessage(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - duration: duration, - action: !showCancel - ? null - : SnackBarAction( - label: LocaleKeys.button_cancel.tr(), - textColor: Colors.white, - onPressed: () { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - }, - ), content: FlowyText( message, - maxLines: 2, - fontSize: UniversalPlatform.isDesktop ? 14 : 12, + color: Theme.of(context).colorScheme.onSurface, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart deleted file mode 100644 index 7a14b6b1c5..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_hub_title.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class NotificationDialog extends StatefulWidget { - const NotificationDialog({ - super.key, - required this.views, - required this.mutex, - }); - - final List views; - final PopoverMutex mutex; - - @override - State createState() => _NotificationDialogState(); -} - -class _NotificationDialogState extends State - with SingleTickerProviderStateMixin { - 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); - } - - void updateState() => setState(() {}); - - @override - void dispose() { - mutex.dispose(); - controller.removeListener(updateState); - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: reminderBloc), - BlocProvider( - create: (_) => NotificationFilterBloc(), - ), - ], - child: BlocBuilder( - builder: (context, filterState) => - BlocBuilder( - builder: (context, state) { - List pastReminders = - state.pastReminders.sortByScheduledAt(); - if (filterState.showUnreadsOnly) { - pastReminders = pastReminders.where((r) => !r.isRead).toList(); - } - - final 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), - Expanded( - child: TabBarView( - controller: controller, - children: [ - NotificationsView( - shownReminders: pastReminders, - reminderBloc: reminderBloc, - views: widget.views, - onAction: onAction, - onReadChanged: _onReadChanged, - actionBar: InboxActionBar( - hasUnreads: hasUnreads, - showUnreadsOnly: filterState.showUnreadsOnly, - ), - ), - NotificationsView( - shownReminders: upcomingReminders, - reminderBloc: reminderBloc, - views: widget.views, - isUpcoming: true, - onAction: onAction, - ), - ], - ), - ), - ], - ); - }, - ), - ), - ); - } - - void onAction(ReminderPB reminder, int? path, ViewPB? view) { - reminderBloc.add( - ReminderEvent.pressReminder(reminderId: reminder.id, path: path), - ); - - widget.mutex.close(); - } - - void _onReadChanged(ReminderPB reminder, bool isRead) { - reminderBloc.add( - ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/reminder_extension.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/reminder_extension.dart deleted file mode 100644 index 3abe163090..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/reminder_extension.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; -import 'package:collection/collection.dart'; - -extension ReminderSort on Iterable { - List sortByScheduledAt() => - sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt)); -} 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 deleted file mode 100644 index 87d839d71a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/flowy_tab.dart +++ /dev/null @@ -1,37 +0,0 @@ -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({ - super.key, - required this.label, - required this.isSelected, - }); - - final String label; - final bool isSelected; - - static const double mobileHeight = 40; - static const EdgeInsets mobilePadding = EdgeInsets.symmetric(horizontal: 12); - - static const double desktopHeight = 26; - static const EdgeInsets desktopPadding = EdgeInsets.symmetric(horizontal: 8); - - @override - Widget build(BuildContext context) { - return Tab( - height: UniversalPlatform.isMobile ? mobileHeight : desktopHeight, - child: Padding( - padding: UniversalPlatform.isMobile ? mobilePadding : desktopPadding, - child: FlowyText.regular( - label, - color: isSelected - ? AFThemeExtension.of(context).textColor - : Theme.of(context).hintColor, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart deleted file mode 100644 index 988ca40fca..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.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/button.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class InboxActionBar extends StatelessWidget { - const InboxActionBar({ - super.key, - required this.hasUnreads, - required this.showUnreadsOnly, - }); - - final bool hasUnreads; - final bool showUnreadsOnly; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: AFThemeExtension.of(context).calloutBGColor, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _MarkAsReadButton( - onMarkAllRead: !hasUnreads - ? null - : () => context - .read() - .add(const ReminderEvent.markAllRead()), - ), - _ToggleUnreadsButton( - showUnreadsOnly: showUnreadsOnly, - onToggled: (_) => context - .read() - .add(const NotificationFilterEvent.toggleShowUnreadsOnly()), - ), - ], - ), - ), - ); - } -} - -class _ToggleUnreadsButton extends StatefulWidget { - const _ToggleUnreadsButton({ - required this.onToggled, - this.showUnreadsOnly = false, - }); - - final Function(bool) onToggled; - final bool showUnreadsOnly; - - @override - State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState(); -} - -class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> { - late bool showUnreadsOnly = widget.showUnreadsOnly; - - @override - Widget build(BuildContext context) { - return SegmentedButton( - onSelectionChanged: (Set newSelection) { - setState(() => showUnreadsOnly = newSelection.first); - widget.onToggled(showUnreadsOnly); - }, - showSelectedIcon: false, - style: ButtonStyle( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: WidgetStatePropertyAll( - BorderSide(color: Theme.of(context).dividerColor), - ), - shape: const WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: Corners.s6Border, - ), - ), - foregroundColor: WidgetStateProperty.resolveWith( - (state) { - if (state.contains(WidgetState.selected)) { - return Theme.of(context).colorScheme.onPrimary; - } - - return AFThemeExtension.of(context).textColor; - }, - ), - backgroundColor: WidgetStateProperty.resolveWith( - (state) { - if (state.contains(WidgetState.selected)) { - return Theme.of(context).colorScheme.primary; - } - - if (state.contains(WidgetState.hovered)) { - return AFThemeExtension.of(context).lightGreyHover; - } - - return Theme.of(context).cardColor; - }, - ), - ), - segments: [ - ButtonSegment( - value: false, - label: Text( - LocaleKeys.notificationHub_actions_showAll.tr(), - style: const TextStyle(fontSize: 12), - ), - ), - ButtonSegment( - value: true, - label: Text( - LocaleKeys.notificationHub_actions_showUnreads.tr(), - style: const TextStyle(fontSize: 12), - ), - ), - ], - selected: {showUnreadsOnly}, - ); - } -} - -class _MarkAsReadButton extends StatefulWidget { - const _MarkAsReadButton({this.onMarkAllRead}); - - final VoidCallback? onMarkAllRead; - - @override - State<_MarkAsReadButton> createState() => _MarkAsReadButtonState(); -} - -class _MarkAsReadButtonState extends State<_MarkAsReadButton> { - bool _isHovering = false; - - @override - Widget build(BuildContext context) { - return Opacity( - opacity: widget.onMarkAllRead != null ? 1 : 0.5, - child: FlowyHover( - onHover: (isHovering) => setState(() => _isHovering = isHovering), - resetHoverOnRebuild: false, - child: FlowyTextButton( - LocaleKeys.notificationHub_actions_markAllRead.tr(), - fontColor: widget.onMarkAllRead != null && _isHovering - ? Theme.of(context).colorScheme.onSurface - : AFThemeExtension.of(context).textColor, - heading: FlowySvg( - FlowySvgs.checklist_s, - color: widget.onMarkAllRead != null && _isHovering - ? Theme.of(context).colorScheme.onSurface - : AFThemeExtension.of(context).textColor, - ), - hoverColor: widget.onMarkAllRead != null && _isHovering - ? Theme.of(context).colorScheme.primary - : null, - onPressed: widget.onMarkAllRead, - ), - ), - ); - } -} 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 deleted file mode 100644 index 6f29c8e2aa..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.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/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: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 NotificationButton extends StatefulWidget { - 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; - - 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, - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - }, - ); - }, - ), - ); - } - - Widget _buildNotificationIcon(BuildContext context, bool hasUnreads) { - return Stack( - children: [ - 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, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart deleted file mode 100644 index 0434cc56d0..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -class NotificationHubTitle extends StatelessWidget { - const NotificationHubTitle({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16) + - const EdgeInsets.only(top: 12, bottom: 4), - child: FlowyText.semibold( - LocaleKeys.notificationHub_title.tr(), - color: Theme.of(context).colorScheme.tertiary, - fontSize: 16, - ), - ); - } -} 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 deleted file mode 100644 index 3d12a6afb7..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart +++ /dev/null @@ -1,296 +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: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:easy_localization/easy_localization.dart'; -import 'package:fixnum/fixnum.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:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class NotificationItem extends StatefulWidget { - const NotificationItem({ - super.key, - required this.reminder, - required this.title, - required this.scheduled, - required this.body, - required this.isRead, - this.block, - this.includeTime = false, - this.readOnly = false, - this.onAction, - this.onReadChanged, - this.view, - }); - - final ReminderPB reminder; - final String title; - final Int64 scheduled; - final String body; - final bool isRead; - final ViewPB? view; - - /// If [block] is provided, then [body] will be shown only if - /// [block] fails to fetch. - /// - /// [block] is rendered as a result of a [FutureBuilder]. - /// - final Future? block; - - final bool includeTime; - final bool readOnly; - - final void Function(int? path)? onAction; - final void Function(bool isRead)? onReadChanged; - - @override - State createState() => _NotificationItemState(); -} - -class _NotificationItemState extends State { - final PopoverMutex mutex = PopoverMutex(); - bool _isHovering = false; - int? path; - - late final String infoString; - - @override - void initState() { - super.initState(); - widget.block?.then((b) => path = b?.path.first); - infoString = _buildInfoString(); - } - - @override - void dispose() { - mutex.dispose(); - super.dispose(); - } - - String _buildInfoString() { - String scheduledString = - _scheduledString(widget.scheduled, widget.includeTime); - - if (widget.view != null) { - scheduledString = '$scheduledString - ${widget.view!.name}'; - } - - return scheduledString; - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => _onHover(true), - onExit: (_) => _onHover(false), - cursor: widget.onAction != null - ? SystemMouseCursors.click - : MouseCursor.defer, - child: Stack( - children: [ - GestureDetector( - onTap: () => widget.onAction?.call(path), - child: AbsorbPointer( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: UniversalPlatform.isMobile - ? BorderSide( - color: AFThemeExtension.of(context).calloutBGColor, - ) - : BorderSide.none, - ), - ), - child: Opacity( - opacity: widget.isRead && !widget.readOnly ? 0.5 : 1, - child: DecoratedBox( - decoration: BoxDecoration( - color: _isHovering && widget.onAction != null - ? AFThemeExtension.of(context).lightGreyHover - : Colors.transparent, - border: widget.isRead || widget.readOnly - ? null - : Border( - left: BorderSide( - width: UniversalPlatform.isMobile ? 4 : 2, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - FlowySvgs.time_s, - size: Size.square( - UniversalPlatform.isMobile ? 24 : 20, - ), - color: AFThemeExtension.of(context).textColor, - ), - const HSpace(16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - widget.title, - fontSize: - UniversalPlatform.isMobile ? 16 : 14, - color: AFThemeExtension.of(context).textColor, - ), - FlowyText.regular( - infoString, - fontSize: - UniversalPlatform.isMobile ? 12 : 10, - ), - const VSpace(5), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: Corners.s8Border, - color: - Theme.of(context).colorScheme.surface, - ), - child: _NotificationContent( - block: widget.block, - reminder: widget.reminder, - body: widget.body, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - ), - ), - if (UniversalPlatform.isMobile && !widget.readOnly || - _isHovering && !widget.readOnly) - Positioned( - right: UniversalPlatform.isMobile ? 8 : 4, - top: UniversalPlatform.isMobile ? 8 : 4, - child: NotificationItemActions( - isRead: widget.isRead, - onReadChanged: widget.onReadChanged, - ), - ), - ], - ), - ); - } - - String _scheduledString(Int64 secondsSinceEpoch, bool includeTime) { - final appearance = context.read().state; - return appearance.dateFormat.formatDate( - DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000), - includeTime, - appearance.timeFormat, - ); - } - - void _onHover(bool isHovering) => setState(() => _isHovering = isHovering); -} - -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 - Widget build(BuildContext context) { - return FutureBuilder( - future: block, - builder: (context, snapshot) { - if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) { - return FlowyText.regular(body, maxLines: 4); - } - - return IntrinsicHeight( - child: NotificationDocumentContent( - nodes: [snapshot.data!], - reminder: reminder, - ), - ); - }, - ); - } -} - -class NotificationItemActions extends StatelessWidget { - const NotificationItemActions({ - super.key, - required this.isRead, - this.onReadChanged, - }); - - final bool isRead; - final void Function(bool isRead)? onReadChanged; - - @override - Widget build(BuildContext context) { - final double size = UniversalPlatform.isMobile ? 40.0 : 30.0; - - return Container( - height: size, - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all( - color: AFThemeExtension.of(context).lightGreyHover, - ), - borderRadius: BorderRadius.circular(6), - ), - child: IntrinsicHeight( - child: Row( - children: [ - if (isRead) ...[ - FlowyIconButton( - height: size, - width: size, - radius: BorderRadius.circular(4), - tooltipText: - LocaleKeys.reminderNotification_tooltipMarkUnread.tr(), - icon: const FlowySvg(FlowySvgs.restore_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - onPressed: () => onReadChanged?.call(false), - ), - ] else ...[ - FlowyIconButton( - height: size, - width: size, - radius: BorderRadius.circular(4), - tooltipText: - LocaleKeys.reminderNotification_tooltipMarkRead.tr(), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - icon: const FlowySvg(FlowySvgs.messages_s), - onPressed: () => onReadChanged?.call(true), - ), - ], - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart deleted file mode 100644 index 099203a90a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class NotificationTabBar extends StatelessWidget { - const NotificationTabBar({super.key, required this.tabController}); - - final TabController tabController; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - ), - ), - ), - child: Row( - children: [ - Expanded( - child: TabBar( - controller: tabController, - padding: const EdgeInsets.symmetric(horizontal: 8), - labelPadding: EdgeInsets.zero, - indicatorSize: TabBarIndicatorSize.label, - indicator: UnderlineTabIndicator( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - isScrollable: true, - tabs: [ - FlowyTabItem( - label: LocaleKeys.notificationHub_tabs_inbox.tr(), - isSelected: tabController.index == 0, - ), - FlowyTabItem( - label: LocaleKeys.notificationHub_tabs_upcoming.tr(), - isSelected: tabController.index == 1, - ), - ], - ), - ), - ], - ), - ); - } -} 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 deleted file mode 100644 index 0465256f60..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/user/application/reminder/reminder_extension.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart'; -import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.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-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/material.dart'; - -/// Displays a Lsit of Notifications, currently used primarily to -/// display Reminders. -/// -/// Optimized for both Mobile & Desktop use -/// -class NotificationsView extends StatelessWidget { - const NotificationsView({ - super.key, - required this.shownReminders, - required this.reminderBloc, - required this.views, - this.isUpcoming = false, - this.onAction, - this.onReadChanged, - this.actionBar, - }); - - final List shownReminders; - final ReminderBloc reminderBloc; - final List views; - final bool isUpcoming; - final Function(ReminderPB reminder, int? path, ViewPB? view)? onAction; - final Function(ReminderPB reminder, bool isRead)? onReadChanged; - final Widget? actionBar; - - @override - Widget build(BuildContext context) { - if (shownReminders.isEmpty) { - return Column( - children: [ - if (actionBar != null) actionBar!, - const Expanded(child: NotificationsHubEmpty()), - ], - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (actionBar != null) actionBar!, - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - ...shownReminders.map( - (ReminderPB reminder) { - final blockId = reminder.meta[ReminderMetaKeys.blockId]; - - final documentService = DocumentService(); - final documentFuture = documentService.openDocument( - documentId: reminder.objectId, - ); - - Future? nodeBuilder; - if (blockId != null) { - nodeBuilder = - _getNodeFromDocument(documentFuture, blockId); - } - - final view = views.findView(reminder.objectId); - return NotificationItem( - reminder: reminder, - key: ValueKey(reminder.id), - title: reminder.title, - scheduled: reminder.scheduledAt, - body: reminder.message, - block: nodeBuilder, - isRead: reminder.isRead, - includeTime: reminder.includeTime ?? false, - readOnly: isUpcoming, - onReadChanged: (isRead) => - onReadChanged?.call(reminder, isRead), - onAction: (path) => onAction?.call(reminder, path, view), - view: view, - ); - }, - ), - ], - ), - ), - ), - ], - ); - } - - Future _getNodeFromDocument( - Future> documentFuture, - String blockId, - ) async { - final document = (await documentFuture).fold( - (document) => document, - (_) => null, - ); - - if (document == null) { - return null; - } - - final rootNode = document.toDocument()?.root; - if (rootNode == null) { - return null; - } - - return _searchById(rootNode, blockId); - } -} - -/// Recursively iterates a [Node] and compares by its [id] -/// -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; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart deleted file mode 100644 index 03eeec921f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.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 NotificationsHubEmpty extends StatelessWidget { - const NotificationsHubEmpty({super.key}); - - @override - Widget build(BuildContext context) { - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - LocaleKeys.notificationHub_emptyTitle.tr(), - fontWeight: FontWeight.w700, - fontSize: 14, - ), - const VSpace(8), - FlowyText.regular( - LocaleKeys.notificationHub_emptyBody.tr(), - textAlign: TextAlign.center, - maxLines: 2, - ), - ], - ), - ), - ); - } -} 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 b836f15b03..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ /dev/null @@ -1,150 +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, - isToggleable: state is ReadyLocalAiPluginState, - ), - collapsed: const SizedBox.shrink(), - expanded: Padding( - padding: EdgeInsets.only(top: 12), - child: LocalAISettingPanel(), - ), - ); - }, - ), - ); - } -} - -class LocalAiSettingHeader extends StatelessWidget { - const LocalAiSettingHeader({ - super.key, - required this.isEnabled, - required this.isToggleable, - }); - - final bool isEnabled; - final bool isToggleable; - - @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, - ), - ], - ), - ), - IgnorePointer( - ignoring: !isToggleable, - child: Opacity( - opacity: isToggleable ? 1 : 0.5, - child: Toggle( - value: isEnabled, - onChanged: (_) => _onToggleChanged(context), - ), - ), - ), - ], - ); - } - - void _onToggleChanged(BuildContext context) { - if (isEnabled) { - 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()); - }, - ); - } else { - 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 deleted file mode 100644 index d7afb03e87..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.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_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category.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:flutter_bloc/flutter_bloc.dart'; - -class SettingsAccountView extends StatefulWidget { - const SettingsAccountView({ - super.key, - required this.userProfile, - required this.didLogin, - required this.didLogout, - }); - - final UserProfilePB userProfile; - - // Called when the user signs in from the setting dialog - final VoidCallback didLogin; - - // Called when the user logout in the setting dialog - final VoidCallback didLogout; - - @override - State createState() => _SettingsAccountViewState(); -} - -class _SettingsAccountViewState extends State { - late String userName = widget.userProfile.name; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - getIt(param1: widget.userProfile) - ..add(const SettingsUserEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return SettingsBody( - title: LocaleKeys.newSettings_myAccount_title.tr(), - children: [ - // user profile - SettingsCategory( - title: LocaleKeys.newSettings_myAccount_myProfile.tr(), - children: [ - AccountUserProfile( - name: userName, - iconUrl: state.userProfile.iconUrl, - onSave: (newName) { - // Pseudo change the name to update the UI before the backend - // processes the request. This is to give the user a sense of - // immediate feedback, and avoid UI flickering. - setState(() => userName = newName); - context - .read() - .add(SettingsUserEvent.updateUserName(name: newName)); - }, - ), - ], - ), - - // user email - // Only show email if the user is authenticated and not using local auth - if (isAuthEnabled && - state.userProfile.workspaceAuthType != AuthTypePB.Local) ...[ - SettingsCategory( - title: LocaleKeys.newSettings_myAccount_myAccount.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, - ), - ], - ), - ], - - 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 - SettingsCategory( - title: LocaleKeys.newSettings_myAccount_aboutAppFlowy.tr(), - children: const [ - SettingsAppVersion(), - ], - ), - - // user deletion - if (widget.userProfile.workspaceAuthType == AuthTypePB.Server) - const AccountDeletionButton(), - ], - ); - }, - ), - ); - } -} 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 deleted file mode 100644 index 77c1116319..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart +++ /dev/null @@ -1,578 +0,0 @@ -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: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:flutter_bloc/flutter_bloc.dart'; - -import '../../../../generated/locale_keys.g.dart'; - -const _buttonsMinWidth = 100.0; - -class SettingsBillingView extends StatefulWidget { - const SettingsBillingView({ - super.key, - required this.workspaceId, - required this.user, - }); - - 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; - } - }, - builder: (context, state) { - return state.map( - initial: (_) => const SizedBox.shrink(), - loading: (_) => const Center( - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator.adaptive(strokeWidth: 3), - ), - ), - error: (state) { - if (state.error != null) { - return Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: AppFlowyErrorPage( - error: state.error!, - ), - ), - ); - } - - return ErrorWidget.withDetails(message: 'Something went wrong!'); - }, - ready: (state) { - final billingPortalEnabled = - state.subscriptionInfo.isBillingPortalEnabled; - - return SettingsBody( - title: LocaleKeys.settings_billingPage_title.tr(), - children: [ - SettingsCategory( - title: LocaleKeys.settings_billingPage_plan_title.tr(), - children: [ - SingleSettingAction( - onPressed: () => _openPricingDialog( - context, - widget.workspaceId, - widget.user.id, - state.subscriptionInfo, - ), - fontWeight: FontWeight.w500, - label: state.subscriptionInfo.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); - }, - 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, - ), - ], - ), - if (billingPortalEnabled) - SettingsCategory( - title: LocaleKeys - .settings_billingPage_paymentDetails_title - .tr(), - children: [ - SingleSettingAction( - onPressed: () => context - .read() - .add( - const SettingsBillingEvent.openCustomerPortal(), - ), - label: LocaleKeys - .settings_billingPage_paymentDetails_methodLabel - .tr(), - fontWeight: FontWeight.w500, - 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(), - ], - ), - ], - ); - }, - ); - }, - ), - ); - } - - void _openPricingDialog( - BuildContext context, - String workspaceId, - Int64 userId, - WorkspaceSubscriptionInfoPB subscriptionInfo, - ) => - showDialog( - context: context, - builder: (_) => BlocProvider( - create: (_) => - SettingsPlanBloc(workspaceId: workspaceId, userId: widget.user.id) - ..add(const SettingsPlanEvent.started()), - child: SettingsPlanComparisonDialog( - workspaceId: workspaceId, - subscriptionInfo: subscriptionInfo, - ), - ), - ).then((didChangePlan) { - if (didChangePlan == true && context.mounted) { - 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 deleted file mode 100644 index a2d911ea40..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ /dev/null @@ -1,456 +0,0 @@ -import 'dart:async'; - -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_body.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/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart'; -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: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/hover.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:fluttertoast/fluttertoast.dart'; - -class SettingsManageDataView extends StatelessWidget { - const SettingsManageDataView({super.key, required this.userProfile}); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SettingsLocationCubit(), - child: BlocBuilder( - builder: (context, state) { - return SettingsBody( - title: LocaleKeys.settings_manageDataPage_title.tr(), - description: LocaleKeys.settings_manageDataPage_description.tr(), - children: [ - SettingsCategory( - title: - LocaleKeys.settings_manageDataPage_dataStorage_title.tr(), - tooltip: - LocaleKeys.settings_manageDataPage_dataStorage_tooltip.tr(), - actions: [ - if (state.mapOrNull(didReceivedPath: (_) => true) == true) - SettingAction( - tooltip: LocaleKeys - .settings_manageDataPage_dataStorage_actions_resetTooltip - .tr(), - icon: const FlowySvg( - FlowySvgs.restore_s, - size: Size.square(20), - ), - label: LocaleKeys.settings_common_reset.tr(), - onPressed: () => showConfirmDialog( - context: context, - confirmLabel: LocaleKeys.button_confirm.tr(), - title: LocaleKeys - .settings_manageDataPage_dataStorage_resetDialog_title - .tr(), - description: LocaleKeys - .settings_manageDataPage_dataStorage_resetDialog_description - .tr(), - onConfirm: () async { - final directory = - await appFlowyApplicationDataDirectory(); - final path = directory.path; - if (!context.mounted || - state.mapOrNull(didReceivedPath: (e) => e.path) == - path) { - return; - } - - await context - .read() - .resetDataStoragePathToApplicationDefault(); - await runAppFlowy(isAnon: true); - }, - ), - ), - ], - children: state - .map( - initial: (_) => [const CircularProgressIndicator()], - didReceivedPath: (event) => [ - _CurrentPath(path: event.path), - _DataPathActions(currentPath: event.path), - ], - ) - .toList(), - ), - SettingsCategory( - title: LocaleKeys.settings_manageDataPage_importData_title.tr(), - tooltip: - LocaleKeys.settings_manageDataPage_importData_tooltip.tr(), - children: const [_ImportDataField()], - ), - if (kDebugMode) ...[ - SettingsCategory( - title: LocaleKeys.settings_files_exportData.tr(), - children: const [ - SettingsExportFileWidget(), - FixDataWidget(), - ], - ), - ], - 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: [ - SingleSettingAction( - labelMaxLines: 4, - label: LocaleKeys.settings_manageDataPage_cache_description - .tr(), - buttonLabel: - LocaleKeys.settings_manageDataPage_cache_title.tr(), - onPressed: () { - showCancelAndConfirmDialog( - context: context, - title: LocaleKeys - .settings_manageDataPage_cache_dialog_title - .tr(), - description: LocaleKeys - .settings_manageDataPage_cache_dialog_description - .tr(), - confirmLabel: LocaleKeys.button_ok.tr(), - onConfirm: () async { - // clear all cache - await getIt().clearAllCache(); - - // check the workspace and space health - await WorkspaceDataManager.checkViewHealth( - dryRun: false, - ); - - if (context.mounted) { - showToastNotification( - message: LocaleKeys - .settings_manageDataPage_cache_dialog_successHint - .tr(), - ); - } - }, - ); - }, - ), - ], - ), - ], - ); - }, - ), - ); - } -} - -// class _EncryptDataSetting extends StatelessWidget { -// const _EncryptDataSetting({required this.userProfile}); - -// final UserProfilePB userProfile; - -// @override -// Widget build(BuildContext context) { -// return BlocProvider.value( -// value: context.read(), -// child: BlocBuilder( -// builder: (context, state) { -// if (state.loadingState?.isLoading() == true) { -// return const Row( -// children: [ -// SizedBox( -// width: 20, -// height: 20, -// child: CircularProgressIndicator( -// strokeWidth: 3, -// ), -// ), -// HSpace(16), -// FlowyText.medium( -// 'Encrypting data...', -// fontSize: 14, -// ), -// ], -// ); -// } - -// if (userProfile.encryptionType == EncryptionTypePB.NoEncryption) { -// return Row( -// children: [ -// SizedBox( -// height: 42, -// child: FlowyTextButton( -// LocaleKeys.settings_manageDataPage_encryption_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: () => SettingsAlertDialog( -// title: LocaleKeys -// .settings_manageDataPage_encryption_dialog_title -// .tr(), -// subtitle: LocaleKeys -// .settings_manageDataPage_encryption_dialog_description -// .tr(), -// confirmLabel: LocaleKeys -// .settings_manageDataPage_encryption_dialog_title -// .tr(), -// implyLeading: true, -// // Generate a secret one time for the user -// confirm: () => context -// .read() -// .add(const EncryptSecretEvent.setEncryptSecret('')), -// ).show(context), -// ), -// ), -// ], -// ); -// } -// // Show encryption secret for copy/save -// return const SizedBox.shrink(); -// }, -// ), -// ); -// } -// } - -class _ImportDataField extends StatefulWidget { - const _ImportDataField(); - - @override - State<_ImportDataField> createState() => _ImportDataFieldState(); -} - -class _ImportDataFieldState extends State<_ImportDataField> { - final _fToast = FToast(); - - @override - void initState() { - super.initState(); - _fToast.init(context); - } - - @override - void dispose() { - _fToast.removeQueuedCustomToasts(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SettingFileImportBloc(), - child: BlocConsumer( - listenWhen: (previous, current) => - previous.successOrFail != current.successOrFail, - listener: (_, state) => state.successOrFail?.fold( - (_) => _showToast(LocaleKeys.settings_menu_importSuccess.tr()), - (_) => _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; - } - - context - .read() - .add(SettingFileImportEvent.importAppFlowyDataFolder(path)); - }, - ); - }, - ), - ); - } - - void _showToast(String message) { - _fToast.showToast( - child: FlowyMessageToast(message: message), - gravity: ToastGravity.CENTER, - ); - } -} - -class _CurrentPath extends StatefulWidget { - const _CurrentPath({required this.path}); - - final String path; - - @override - State<_CurrentPath> createState() => _CurrentPathState(); -} - -class _CurrentPathState extends State<_CurrentPath> { - Timer? linkCopiedTimer; - bool showCopyMessage = false; - - @override - void dispose() { - linkCopiedTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isLM = Theme.of(context).isLightMode; - - return Column( - children: [ - Row( - children: [ - Expanded( - child: Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: (_) => _copyLink(widget.path), - child: FlowyHover( - style: const HoverStyle.transparent(), - resetHoverOnRebuild: false, - builder: (_, isHovering) => FlowyText.regular( - widget.path, - maxLines: 2, - overflow: TextOverflow.ellipsis, - lineHeight: 1.5, - decoration: isHovering ? TextDecoration.underline : null, - color: isLM - ? const Color(0xFF005483) - : Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - const HSpace(8), - showCopyMessage - ? SizedBox( - height: 36, - child: FlowyTextButton( - LocaleKeys - .settings_manageDataPage_dataStorage_actions_copiedHint - .tr(), - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - fontWeight: FontWeight.w500, - radius: BorderRadius.circular(12), - fillColor: AFThemeExtension.of(context).tint7, - hoverColor: AFThemeExtension.of(context).tint7, - ), - ) - : Padding( - padding: const EdgeInsets.only(left: 100), - child: SettingAction( - tooltip: LocaleKeys - .settings_manageDataPage_dataStorage_actions_copy - .tr(), - icon: const FlowySvg( - FlowySvgs.copy_s, - size: Size.square(24), - ), - onPressed: () => _copyLink(widget.path), - ), - ), - ], - ), - ], - ); - } - - void _copyLink(String? path) { - AppFlowyClipboard.setData(text: path); - setState(() => showCopyMessage = true); - linkCopiedTimer?.cancel(); - linkCopiedTimer = Timer( - const Duration(milliseconds: 300), - () => mounted ? setState(() => showCopyMessage = false) : null, - ); - } -} - -class _DataPathActions extends StatelessWidget { - const _DataPathActions({required this.currentPath}); - - final String currentPath; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - height: 42, - child: PrimaryRoundedButton( - text: LocaleKeys.settings_manageDataPage_dataStorage_actions_change - .tr(), - margin: const EdgeInsets.symmetric(horizontal: 24), - fontWeight: FontWeight.w600, - radius: 12.0, - onTap: () async { - final path = await getIt().getDirectoryPath(); - if (!context.mounted || path == null || currentPath == path) { - return; - } - - await context.read().setCustomPath(path); - await runAppFlowy(isAnon: true); - - if (context.mounted) Navigator.of(context).pop(); - }, - ), - ), - const HSpace(16), - SettingAction( - tooltip: LocaleKeys - .settings_manageDataPage_dataStorage_actions_openTooltip - .tr(), - label: - LocaleKeys.settings_manageDataPage_dataStorage_actions_open.tr(), - icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(20)), - onPressed: () => afLaunchUri(Uri.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 deleted file mode 100644 index 420daa8698..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart +++ /dev/null @@ -1,766 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/util/theme_extension.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/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/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 '../../../../generated/locale_keys.g.dart'; - -class SettingsPlanComparisonDialog extends StatefulWidget { - const SettingsPlanComparisonDialog({ - super.key, - required this.workspaceId, - required this.subscriptionInfo, - }); - - final String workspaceId; - final WorkspaceSubscriptionInfoPB subscriptionInfo; - - @override - State createState() => - _SettingsPlanComparisonDialogState(); -} - -class _SettingsPlanComparisonDialogState - extends State { - final horizontalController = ScrollController(); - final verticalController = ScrollController(); - - late WorkspaceSubscriptionInfoPB currentInfo = widget.subscriptionInfo; - - Loading? loadingIndicator; - - @override - void dispose() { - horizontalController.dispose(); - verticalController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isLM = Theme.of(context).isLightMode; - - return BlocConsumer( - listener: (context, state) { - final readyState = state.mapOrNull(ready: (state) => state); - - if (readyState == null) { - 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 - .settings_comparePlanDialog_paymentSuccess_description - .tr(args: [readyState.successfulPlanUpgrade!.label]), - confirmLabel: LocaleKeys.button_close.tr(), - onConfirm: () {}, - ); - } - - setState(() => currentInfo = readyState.subscriptionInfo); - }, - builder: (context, state) => FlowyDialog( - constraints: const BoxConstraints(maxWidth: 784, minWidth: 674), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24, left: 24, right: 24), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - 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, - ), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowySvg( - FlowySvgs.m_close_m, - size: const Size.square(20), - color: AFThemeExtension.of(context).strongText, - ), - ), - ), - ], - ), - ), - const VSpace(16), - Flexible( - child: SingleChildScrollView( - controller: horizontalController, - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - controller: verticalController, - padding: const EdgeInsets.only( - left: 24, - right: 24, - bottom: 24, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 250, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(30), - SizedBox( - height: 116, - child: FlowyText.semibold( - LocaleKeys - .settings_comparePlanDialog_planFeatures - .tr(), - fontSize: 24, - maxLines: 2, - color: isLM - ? const Color(0xFF5C3699) - : const Color(0xFFE8E0FF), - ), - ), - const SizedBox(height: 116), - const SizedBox(height: 56), - ..._planLabels.map( - (e) => _ComparisonCell( - label: e.label, - tooltip: e.tooltip, - ), - ), - ], - ), - ), - _PlanTable( - title: LocaleKeys - .settings_comparePlanDialog_freePlan_title - .tr(), - description: LocaleKeys - .settings_comparePlanDialog_freePlan_description - .tr(), - price: LocaleKeys - .settings_comparePlanDialog_freePlan_price - .tr( - args: [ - SubscriptionPlanPB.Free.priceMonthBilling, - ], - ), - priceInfo: LocaleKeys - .settings_comparePlanDialog_freePlan_priceInfo - .tr(), - cells: _freeLabels, - isCurrent: - currentInfo.plan == WorkspacePlanPB.FreePlan, - buttonType: WorkspacePlanPB.FreePlan.buttonTypeFor( - currentInfo.plan, - ), - onSelected: () async { - if (currentInfo.plan == - WorkspacePlanPB.FreePlan || - currentInfo.isCanceled) { - return; - } - - final reason = - await showCancelSurveyDialog(context); - if (reason == null || !context.mounted) { - return; - } - - await showConfirmDialog( - context: context, - title: LocaleKeys - .settings_comparePlanDialog_downgradeDialog_title - .tr(args: [currentInfo.label]), - description: LocaleKeys - .settings_comparePlanDialog_downgradeDialog_description - .tr(), - confirmLabel: LocaleKeys - .settings_comparePlanDialog_downgradeDialog_downgradeLabel - .tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: () => - context.read().add( - SettingsPlanEvent.cancelSubscription( - reason: reason, - ), - ), - ); - }, - ), - _PlanTable( - title: LocaleKeys - .settings_comparePlanDialog_proPlan_title - .tr(), - description: LocaleKeys - .settings_comparePlanDialog_proPlan_description - .tr(), - price: LocaleKeys - .settings_comparePlanDialog_proPlan_price - .tr( - args: [SubscriptionPlanPB.Pro.priceAnnualBilling], - ), - priceInfo: LocaleKeys - .settings_comparePlanDialog_proPlan_priceInfo - .tr( - args: [SubscriptionPlanPB.Pro.priceMonthBilling], - ), - cells: _proLabels, - isCurrent: - currentInfo.plan == WorkspacePlanPB.ProPlan, - buttonType: WorkspacePlanPB.ProPlan.buttonTypeFor( - currentInfo.plan, - ), - onSelected: () => - context.read().add( - const SettingsPlanEvent.addSubscription( - SubscriptionPlanPB.Pro, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -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, - required this.description, - required this.price, - required this.priceInfo, - required this.cells, - required this.isCurrent, - required this.onSelected, - this.buttonType = _PlanButtonType.none, - }); - - final String title; - final String description; - final String price; - final String priceInfo; - - final List<_CellItem> cells; - final bool isCurrent; - final VoidCallback onSelected; - final _PlanButtonType buttonType; - - @override - Widget build(BuildContext context) { - final highlightPlan = !isCurrent && buttonType == _PlanButtonType.upgrade; - final isLM = Theme.of(context).isLightMode; - - return Container( - width: 215, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - gradient: !highlightPlan - ? null - : LinearGradient( - colors: [ - isLM ? const Color(0xFF251D37) : const Color(0xFF7459AD), - isLM ? const Color(0xFF7547C0) : const Color(0xFFDDC8FF), - ], - ), - ), - padding: !highlightPlan - ? const EdgeInsets.only(top: 4) - : const EdgeInsets.all(4), - child: Container( - padding: isCurrent - ? const EdgeInsets.only(bottom: 22) - : const EdgeInsets.symmetric(vertical: 22), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(22), - color: Theme.of(context).cardColor, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isCurrent) const _CurrentBadge(), - const VSpace(4), - _Heading( - title: title, - description: description, - isPrimary: !highlightPlan, - ), - _Heading( - title: price, - description: priceInfo, - isPrimary: !highlightPlan, - ), - if (buttonType == _PlanButtonType.none) ...[ - const SizedBox(height: 56), - ] else ...[ - Opacity( - opacity: 1, - child: Padding( - padding: EdgeInsets.only( - left: 12 + (buttonType.isUpgrade ? 12 : 0), - ), - child: _ActionButton( - label: buttonType.isUpgrade - ? LocaleKeys.settings_comparePlanDialog_actions_upgrade - .tr() - : LocaleKeys - .settings_comparePlanDialog_actions_downgrade - .tr(), - onPressed: onSelected, - isUpgrade: buttonType.isUpgrade, - useGradientBorder: buttonType.isUpgrade, - ), - ), - ), - ], - ...cells.map( - (cell) => _ComparisonCell( - label: cell.label, - icon: cell.icon, - isHighlighted: highlightPlan, - ), - ), - ], - ), - ), - ); - } -} - -class _CurrentBadge extends StatelessWidget { - const _CurrentBadge(); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(left: 12), - height: 22, - width: 72, - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0xFF4F3F5F) - : const Color(0xFFE8E0FF), - 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, - ), - ), - ); - } -} - -class _ComparisonCell extends StatelessWidget { - const _ComparisonCell({ - this.label, - this.icon, - this.tooltip, - this.isHighlighted = false, - }); - - final String? label; - final FlowySvgData? icon; - final String? tooltip; - final bool isHighlighted; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12) + - EdgeInsets.only(left: isHighlighted ? 12 : 0), - height: 36, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - ), - ), - ), - child: Row( - children: [ - if (icon != null) ...[ - FlowySvg( - icon!, - color: AFThemeExtension.of(context).strongText, - ), - ] else if (label != null) ...[ - Expanded( - child: FlowyText.medium( - 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, - ), - ), - ], - ), - ); - } -} - -class _ActionButton extends StatelessWidget { - const _ActionButton({ - required this.label, - required this.onPressed, - required this.isUpgrade, - this.useGradientBorder = false, - }); - - final String label; - final VoidCallback? onPressed; - final bool isUpgrade; - final bool useGradientBorder; - - @override - Widget build(BuildContext context) { - final isLM = Theme.of(context).isLightMode; - - 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), - ), - child: Center(child: _drawText(label, isLM, isUpgrade)), - ), - ), - ), - ), - ], - ), - ); - } - - Widget _drawText(String text, bool isLM, bool isUpgrade) { - final child = FlowyText( - text, - fontSize: 14, - lineHeight: 1.2, - fontWeight: useGradientBorder ? FontWeight.w600 : FontWeight.w500, - color: isUpgrade ? const Color(0xFFC49BEC) : null, - ); - - if (!useGradientBorder || !isLM) { - return child; - } - - return ShaderMask( - blendMode: BlendMode.srcIn, - shaderCallback: (bounds) => const LinearGradient( - transform: GradientRotation(-1.55), - stops: [0.4, 1], - 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, - }) { - 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)), - borderRadius: BorderRadius.circular(16), - ), - child: child, - ); - } -} - -class _Heading extends StatelessWidget { - const _Heading({ - required this.title, - this.description, - this.isPrimary = true, - }); - - final String title; - final String? description; - final bool isPrimary; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 185, - height: 116, - 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), - ), - ), - ], - ), - if (description != null && description!.isNotEmpty) ...[ - const VSpace(4), - Flexible( - child: FlowyText.regular( - description!, - fontSize: 12, - maxLines: 5, - lineHeight: 1.5, - ), - ), - ], - ], - ), - ), - ); - } -} - -class _PlanItem { - const _PlanItem({required this.label, this.tooltip}); - - final String label; - final String? tooltip; -} - -final _planLabels = [ - _PlanItem( - label: LocaleKeys.settings_comparePlanDialog_planLabels_itemOne.tr(), - ), - _PlanItem( - label: LocaleKeys.settings_comparePlanDialog_planLabels_itemTwo.tr(), - ), - _PlanItem( - label: LocaleKeys.settings_comparePlanDialog_planLabels_itemThree.tr(), - ), - _PlanItem( - label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFour.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(), - ), -]; - -class _CellItem { - const _CellItem({this.label, this.icon}); - - final String? label; - final FlowySvgData? icon; -} - -final List<_CellItem> _freeLabels = [ - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemOne.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemTwo.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemThree.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(), - icon: FlowySvgs.check_m, - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.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: '', - ), -]; - -final List<_CellItem> _proLabels = [ - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemOne.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemTwo.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemThree.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(), - icon: FlowySvgs.check_m, - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.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, - ), -]; 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 deleted file mode 100644 index 21896ead0e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ /dev/null @@ -1,924 +0,0 @@ -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'; -import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; -import 'package:appflowy/workspace/application/settings/plan/workspace_usage_ext.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart'; -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_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:flutter_bloc/flutter_bloc.dart'; - -class SettingsPlanView extends StatefulWidget { - const SettingsPlanView({ - super.key, - required this.workspaceId, - required this.user, - }); - - 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, - )..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; - } - }, - builder: (context, state) { - return state.map( - initial: (_) => const SizedBox.shrink(), - loading: (_) => const Center( - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator.adaptive(strokeWidth: 3), - ), - ), - error: (state) { - if (state.error != null) { - return Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: AppFlowyErrorPage( - error: state.error!, - ), - ), - ); - } - - 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), - ], - ), - ], - ), - ); - }, - ), - ); - } -} - -class _CurrentPlanBox extends StatefulWidget { - const _CurrentPlanBox({required this.subscriptionInfo}); - - 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(); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Container( - margin: const EdgeInsets.only(top: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all(color: const Color(0xFFBDBDBD)), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - 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, - ), - ], - ), - ), - 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, - ), - ), - ), - ], - ), - ), - ], - ), - 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, - ), - ], - ], - ), - ), - Positioned( - 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), - ), - ), - child: Center( - child: FlowyText.semibold( - LocaleKeys.settings_planPage_planUsage_currentPlan_bannerLabel - .tr(), - fontSize: 14, - color: Colors.white, - ), - ), - ), - ), - ], - ); - } - - String _canceledDate(BuildContext context) { - final appearance = context.read().state; - return appearance.dateFormat.formatDate( - widget.subscriptionInfo.planSubscription.endDate.toDateTime(), - false, - ); - } - - void _openPricingDialog( - BuildContext context, - String workspaceId, - WorkspaceSubscriptionInfoPB subscriptionInfo, - ) => - showDialog( - context: context, - builder: (_) => BlocProvider.value( - value: planBloc, - child: SettingsPlanComparisonDialog( - workspaceId: workspaceId, - subscriptionInfo: subscriptionInfo, - ), - ), - ); -} - -class _PlanUsageSummary extends StatelessWidget { - const _PlanUsageSummary({ - required this.usage, - required this.subscriptionInfo, - }); - - final WorkspaceUsagePB usage; - final WorkspaceSubscriptionInfoPB subscriptionInfo; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - LocaleKeys.settings_planPage_planUsage_title.tr(), - maxLines: 2, - fontSize: 16, - overflow: TextOverflow.ellipsis, - color: AFThemeExtension.of(context).secondaryTextColor, - ), - 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(), - ), - ), - Expanded( - child: _UsageBox( - title: - LocaleKeys.settings_planPage_planUsage_aiResponseLabel.tr(), - label: - LocaleKeys.settings_planPage_planUsage_aiResponseUsage.tr( - args: [ - usage.aiResponsesCount.toString(), - usage.aiResponsesCountLimit.toString(), - ], - ), - unlimitedLabel: LocaleKeys - .settings_planPage_planUsage_unlimitedAILabel - .tr(), - unlimited: usage.aiResponsesUnlimited, - value: usage.aiResponsesCount.toInt() / - usage.aiResponsesCountLimit.toInt(), - ), - ), - ], - ), - const VSpace(16), - SeparatedColumn( - 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), () {}); - }, - ), - ], - ], - ), - ], - ); - } -} - -class _UsageBox extends StatelessWidget { - const _UsageBox({ - 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( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - title, - 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), - ], - ], - ); - } -} - -class _ToggleMore extends StatefulWidget { - const _ToggleMore({ - required this.value, - required this.label, - this.badgeLabel, - this.onTap, - }); - - final bool value; - final String label; - final String? badgeLabel; - final Future Function()? onTap; - - @override - State<_ToggleMore> createState() => _ToggleMoreState(); -} - -class _ToggleMoreState extends State<_ToggleMore> { - late bool toggleValue = widget.value; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Toggle( - value: toggleValue, - padding: EdgeInsets.zero, - onChanged: (_) async { - if (widget.onTap == null || toggleValue) { - return; - } - - setState(() => toggleValue = !toggleValue); - await widget.onTap!(); - - if (mounted) { - setState(() => toggleValue = !toggleValue); - } - }, - ), - const HSpace(10), - FlowyText.regular( - widget.label, - fontSize: 14, - color: AFThemeExtension.of(context).strongText, - ), - if (widget.badgeLabel != null && widget.badgeLabel!.isNotEmpty) ...[ - const HSpace(10), - SizedBox( - height: 26, - child: Badge( - padding: const EdgeInsets.symmetric(horizontal: 10), - backgroundColor: context.proSecondaryColor, - label: FlowyText.semibold( - widget.badgeLabel!, - fontSize: 12, - color: context.proPrimaryColor, - ), - ), - ), - ], - ], - ); - } -} - -class _PlanProgressIndicator extends StatelessWidget { - const _PlanProgressIndicator({required this.label, required this.progress}); - - final String label; - final double progress; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Row( - children: [ - Expanded( - child: Container( - height: 8, - decoration: BoxDecoration( - 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, - ), - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Stack( - children: [ - FractionallySizedBox( - widthFactor: progress, - child: Container( - decoration: BoxDecoration( - color: progress >= 1 - ? theme.colorScheme.error - : theme.colorScheme.primary, - ), - ), - ), - ], - ), - ), - ), - ), - const HSpace(8), - FlowyText.medium( - label, - fontSize: 11, - color: AFThemeExtension.of(context).secondaryTextColor, - ), - const HSpace(16), - ], - ); - } -} - -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(); - -// @override -// Widget build(BuildContext context) { -// final isLM = Theme.of(context).brightness == Brightness.light; - -// return Container( -// clipBehavior: Clip.antiAlias, -// decoration: BoxDecoration( -// gradient: LinearGradient( -// stops: isLM ? null : [.2, .3, .6], -// transform: isLM ? null : const GradientRotation(-.9), -// begin: isLM ? Alignment.centerLeft : Alignment.topRight, -// end: isLM ? Alignment.centerRight : Alignment.bottomLeft, -// colors: [ -// isLM -// ? const Color(0xFF7547C0).withAlpha(60) -// : const Color(0xFF7547C0), -// if (!isLM) const Color.fromARGB(255, 94, 57, 153), -// isLM -// ? const Color(0xFF251D37).withAlpha(60) -// : const Color(0xFF251D37), -// ], -// ), -// borderRadius: BorderRadius.circular(16), -// ), -// child: Stack( -// children: [ -// Padding( -// padding: const EdgeInsets.all(16), -// child: Row( -// children: [ -// Expanded( -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// const VSpace(18), -// FlowyText.semibold( -// LocaleKeys.settings_planPage_planUsage_deal_title.tr(), -// fontSize: 24, -// color: Theme.of(context).colorScheme.tertiary, -// ), -// const VSpace(8), -// FlowyText.medium( -// LocaleKeys.settings_planPage_planUsage_deal_info.tr(), -// maxLines: 6, -// color: Theme.of(context).colorScheme.tertiary, -// ), -// const VSpace(8), -// FlowyGradientButton( -// label: LocaleKeys -// .settings_planPage_planUsage_deal_viewPlans -// .tr(), -// fontWeight: FontWeight.w500, -// backgroundColor: isLM ? null : Colors.white, -// textColor: isLM -// ? Colors.white -// : Theme.of(context).colorScheme.onPrimary, -// ), -// ], -// ), -// ), -// ], -// ), -// ), -// Positioned( -// right: 0, -// top: 9, -// child: Container( -// height: 32, -// padding: const EdgeInsets.symmetric(horizontal: 16), -// decoration: BoxDecoration( -// gradient: LinearGradient( -// transform: const GradientRotation(.7), -// colors: [ -// if (isLM) const Color(0xFF7156DF), -// isLM -// ? const Color(0xFF3B2E8A) -// : const Color(0xFFCE006F).withAlpha(150), -// isLM ? const Color(0xFF261A48) : const Color(0xFF431459), -// ], -// ), -// ), -// child: Center( -// child: FlowyText.semibold( -// LocaleKeys.settings_planPage_planUsage_deal_bannerLabel.tr(), -// fontSize: 16, -// color: Colors.white, -// ), -// ), -// ), -// ), -// ], -// ), -// ); -// } -// } - -/// Uncomment if we need it in the future -// class _AddAICreditBox extends StatelessWidget { -// const _AddAICreditBox(); - -// @override -// Widget build(BuildContext context) { -// return DecoratedBox( -// decoration: BoxDecoration( -// border: Border.all(color: const Color(0xFFBDBDBD)), -// borderRadius: BorderRadius.circular(16), -// ), -// child: Padding( -// padding: const EdgeInsets.all(16), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// FlowyText.semibold( -// LocaleKeys.settings_planPage_planUsage_aiCredit_title.tr(), -// fontSize: 18, -// color: AFThemeExtension.of(context).secondaryTextColor, -// ), -// const VSpace(8), -// Row( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Flexible( -// flex: 5, -// child: ConstrainedBox( -// constraints: const BoxConstraints(maxWidth: 180), -// child: Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// FlowyText.semibold( -// LocaleKeys.settings_planPage_planUsage_aiCredit_price -// .tr(args: ['5\$]), -// fontSize: 24, -// ), -// FlowyText.medium( -// LocaleKeys -// .settings_planPage_planUsage_aiCredit_priceDescription -// .tr(), -// fontSize: 14, -// color: -// AFThemeExtension.of(context).secondaryTextColor, -// ), -// const VSpace(8), -// FlowyGradientButton( -// label: LocaleKeys -// .settings_planPage_planUsage_aiCredit_purchase -// .tr(), -// ), -// ], -// ), -// ), -// ), -// const HSpace(16), -// Flexible( -// flex: 6, -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// mainAxisSize: MainAxisSize.min, -// children: [ -// FlowyText.regular( -// LocaleKeys.settings_planPage_planUsage_aiCredit_info -// .tr(), -// overflow: TextOverflow.ellipsis, -// maxLines: 5, -// ), -// const VSpace(8), -// SeparatedColumn( -// separatorBuilder: () => const VSpace(4), -// children: [ -// _AIStarItem( -// label: LocaleKeys -// .settings_planPage_planUsage_aiCredit_infoItemOne -// .tr(), -// ), -// _AIStarItem( -// label: LocaleKeys -// .settings_planPage_planUsage_aiCredit_infoItemTwo -// .tr(), -// ), -// ], -// ), -// ], -// ), -// ), -// ], -// ), -// ], -// ), -// ), -// ); -// } -// } - -/// Uncomment if we need it in the future -// class _AIStarItem extends StatelessWidget { -// const _AIStarItem({required this.label}); - -// final String label; - -// @override -// Widget build(BuildContext context) { -// return Row( -// children: [ -// const FlowySvg(FlowySvgs.ai_star_s, color: Color(0xFF750D7E)), -// const HSpace(4), -// Expanded(child: FlowyText(label, maxLines: 2)), -// ], -// ); -// } -// } 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 deleted file mode 100644 index 0d3716c7dc..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart +++ /dev/null @@ -1,756 +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/align_toolbar_item/custom_text_align_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; -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/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'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.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/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:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class SettingsShortcutsView extends StatefulWidget { - const SettingsShortcutsView({super.key}); - - @override - State createState() => _SettingsShortcutsViewState(); -} - -class _SettingsShortcutsViewState extends State { - String _query = ''; - bool _isEditing = false; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => - ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(), - child: Builder( - builder: (context) => SettingsBody( - title: LocaleKeys.settings_shortcutsPage_title.tr(), - autoSeparate: false, - children: [ - Row( - children: [ - Flexible( - child: _SearchBar( - onSearchChanged: (v) => setState(() => _query = v), - ), - ), - 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, - ); - }, - ), - ], - ), - BlocBuilder( - builder: (context, state) { - final filtered = state.commandShortcutEvents - .where( - (e) => e.afLabel - .toLowerCase() - .contains(_query.toLowerCase()), - ) - .toList(); - - return Column( - children: [ - const VSpace(16), - if (state.status.isLoading) ...[ - const CircularProgressIndicator(), - ] else if (state.status.isFailure) ...[ - FlowyErrorPage.message( - LocaleKeys.settings_shortcutsPage_errorPage_message - .tr(args: [state.error]), - howToFix: LocaleKeys - .settings_shortcutsPage_errorPage_howToFix - .tr(), - ), - ] else ...[ - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: filtered.length, - itemBuilder: (context, index) => ShortcutSettingTile( - command: filtered[index], - canStartEditing: () => !_isEditing, - onStartEditing: () => - setState(() => _isEditing = true), - onFinishEditing: () => - setState(() => _isEditing = false), - ), - ), - ], - ], - ); - }, - ), - ], - ), - ), - ); - } -} - -class _SearchBar extends StatelessWidget { - const _SearchBar({this.onSearchChanged}); - - final void Function(String)? onSearchChanged; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 36, - child: FlowyTextField( - onChanged: onSearchChanged, - textStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w400, - ), - decoration: InputDecoration( - hintText: LocaleKeys.settings_shortcutsPage_searchHint.tr(), - counterText: '', - contentPadding: const EdgeInsets.symmetric( - vertical: 9, - horizontal: 16, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: Corners.s12Border, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: Corners.s12Border, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: Corners.s12Border, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: Corners.s12Border, - ), - ), - ), - ); - } -} - -class _ResetButton extends StatelessWidget { - const _ResetButton({this.onReset}); - - final void Function()? onReset; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onReset, - child: FlowyHover( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 6, - ), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.restore_s, - size: Size.square(20), - ), - const HSpace(6), - SizedBox( - height: 16, - child: FlowyText.regular( - LocaleKeys.settings_shortcutsPage_actions_resetDefault.tr(), - color: AFThemeExtension.of(context).strongText, - ), - ), - ], - ), - ), - ), - ); - } -} - -class ShortcutSettingTile extends StatefulWidget { - const ShortcutSettingTile({ - super.key, - required this.command, - required this.onStartEditing, - required this.onFinishEditing, - required this.canStartEditing, - }); - - final CommandShortcutEvent command; - final VoidCallback onStartEditing; - final VoidCallback onFinishEditing; - final bool Function() canStartEditing; - - @override - State createState() => _ShortcutSettingTileState(); -} - -class _ShortcutSettingTileState extends State { - final keybindController = TextEditingController(); - - late final FocusNode focusNode; - - bool isHovering = false; - bool isEditing = false; - bool canClickOutside = false; - - @override - void initState() { - super.initState(); - focusNode = FocusNode( - onKeyEvent: (focusNode, key) { - if (key is! KeyDownEvent && key is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - if (key.logicalKey == LogicalKeyboardKey.enter && - !HardwareKeyboard.instance.isShiftPressed) { - if (keybindController.text == widget.command.command) { - _finishEditing(); - return KeyEventResult.handled; - } - - final conflict = context.read().getConflict( - widget.command, - keybindController.text, - ); - - if (conflict != null) { - canClickOutside = true; - SettingsAlertDialog( - title: LocaleKeys.settings_shortcutsPage_conflictDialog_title - .tr(args: [keybindController.text]), - confirm: () { - conflict.clearCommand(); - _updateCommand(); - Navigator.of(context).pop(); - }, - confirmLabel: LocaleKeys - .settings_shortcutsPage_conflictDialog_confirmLabel - .tr(), - children: [ - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - text: LocaleKeys - .settings_shortcutsPage_conflictDialog_descriptionPrefix - .tr(), - ), - TextSpan( - text: conflict.afLabel, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: LocaleKeys - .settings_shortcutsPage_conflictDialog_descriptionSuffix - .tr(args: [keybindController.text]), - ), - ], - ), - ), - ], - ).show(context).then((_) => canClickOutside = false); - } else { - _updateCommand(); - } - } else if (key.logicalKey == LogicalKeyboardKey.escape) { - _finishEditing(); - } else { - // Extract complete keybinding - setState(() => keybindController.text = key.toCommand); - } - - return KeyEventResult.handled; - }, - ); - } - - void _finishEditing() => setState(() { - isEditing = false; - keybindController.clear(); - widget.onFinishEditing(); - }); - - void _updateCommand() { - widget.command.updateCommand(command: keybindController.text); - context.read().updateAllShortcuts(); - _finishEditing(); - } - - @override - void dispose() { - focusNode.dispose(); - keybindController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 4), - decoration: BoxDecoration( - border: Border( - top: BorderSide(color: Theme.of(context).dividerColor), - ), - ), - child: FlowyHover( - cursor: MouseCursor.defer, - style: HoverStyle( - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.zero, - ), - resetHoverOnRebuild: false, - builder: (context, isHovering) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - const HSpace(8), - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 10), - child: FlowyText.regular( - widget.command.afLabel, - fontSize: 14, - lineHeight: 1, - maxLines: 2, - color: AFThemeExtension.of(context).strongText, - ), - ), - ), - Expanded( - child: isEditing - ? _renderKeybindEditor() - : _renderKeybindings(isHovering), - ), - ], - ), - ), - ), - ); - } - - Widget _renderKeybindings(bool isHovering) => Row( - children: [ - if (widget.command.keybindings.isNotEmpty) ...[ - ..._toParts(widget.command.keybindings.first).map( - (key) => KeyBadge(keyLabel: key), - ), - ] else ...[ - const SizedBox(height: 24), - ], - const Spacer(), - if (isHovering) - GestureDetector( - onTap: () { - if (widget.canStartEditing()) { - setState(() { - widget.onStartEditing(); - isEditing = true; - }); - } - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowyTooltip( - message: LocaleKeys.settings_shortcutsPage_editTooltip.tr(), - child: const FlowySvg( - FlowySvgs.edit_s, - size: Size.square(16), - ), - ), - ), - ), - const HSpace(8), - ], - ); - - Widget _renderKeybindEditor() => TapRegion( - onTapOutside: canClickOutside ? null : (_) => _finishEditing(), - child: FlowyTextField( - focusNode: focusNode, - controller: keybindController, - hintText: LocaleKeys.settings_shortcutsPage_editBindingHint.tr(), - onChanged: (_) => setState(() {}), - suffixIcon: keybindController.text.isNotEmpty - ? MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => setState(() => keybindController.clear()), - child: const FlowySvg( - FlowySvgs.close_s, - size: Size.square(10), - ), - ), - ) - : null, - ), - ); - - List _toParts(Keybinding binding) { - final List keys = []; - - if (binding.isControlPressed) { - keys.add('ctrl'); - } - if (binding.isMetaPressed) { - keys.add('meta'); - } - if (binding.isShiftPressed) { - keys.add('shift'); - } - if (binding.isAltPressed) { - keys.add('alt'); - } - - return keys..add(binding.keyLabel); - } -} - -@visibleForTesting -class KeyBadge extends StatelessWidget { - const KeyBadge({super.key, required this.keyLabel}); - - final String keyLabel; - - @override - Widget build(BuildContext context) { - if (iconData == null && keyLabel.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - height: 24, - margin: const EdgeInsets.only(right: 4), - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: AFThemeExtension.of(context).greySelect, - borderRadius: Corners.s4Border, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.25), - blurRadius: 1, - offset: const Offset(0, 1), - ), - ], - ), - child: Center( - child: iconData != null - ? FlowySvg(iconData!, color: Colors.black) - : FlowyText.medium( - keyLabel.toLowerCase(), - fontSize: 12, - color: Colors.black, - ), - ), - ); - } - - FlowySvgData? get iconData => switch (keyLabel) { - 'meta' => FlowySvgs.keyboard_meta_s, - 'arrow left' => FlowySvgs.keyboard_arrow_left_s, - 'arrow right' => FlowySvgs.keyboard_arrow_right_s, - 'arrow up' => FlowySvgs.keyboard_arrow_up_s, - 'arrow down' => FlowySvgs.keyboard_arrow_down_s, - 'shift' => FlowySvgs.keyboard_shift_s, - 'tab' => FlowySvgs.keyboard_tab_s, - 'enter' || 'return' => FlowySvgs.keyboard_return_s, - 'opt' || 'option' => FlowySvgs.keyboard_option_s, - _ => null, - }; -} - -extension ToCommand on KeyEvent { - String get toCommand { - String command = ''; - if (HardwareKeyboard.instance.isControlPressed) { - command += 'ctrl+'; - } - if (HardwareKeyboard.instance.isMetaPressed) { - command += 'meta+'; - } - if (HardwareKeyboard.instance.isShiftPressed) { - command += 'shift+'; - } - if (HardwareKeyboard.instance.isAltPressed) { - command += 'alt+'; - } - - if ([ - LogicalKeyboardKey.control, - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.controlRight, - LogicalKeyboardKey.meta, - LogicalKeyboardKey.metaLeft, - LogicalKeyboardKey.metaRight, - LogicalKeyboardKey.alt, - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.altRight, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.shiftLeft, - LogicalKeyboardKey.shiftRight, - ].contains(logicalKey)) { - return command; - } - - final keyPressed = keyToCodeMapping.keys.firstWhere( - (k) => keyToCodeMapping[k] == logicalKey.keyId, - orElse: () => '', - ); - - return command += keyPressed; - } -} - -extension CommandLabel on CommandShortcutEvent { - String get afLabel { - String? label; - - if (key == toggleToggleListCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_toggleToDoList.tr(); - } else if (key == insertNewParagraphNextToCodeBlockCommand('').key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_insertNewParagraphInCodeblock - .tr(); - } else if (key == pasteInCodeblock('').key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_pasteInCodeblock.tr(); - } else if (key == selectAllInCodeBlockCommand('').key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_selectAllCodeblock.tr(); - } else if (key == tabToInsertSpacesInCodeBlockCommand('').key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_indentLineCodeblock - .tr(); - } else if (key == tabToDeleteSpacesInCodeBlockCommand('').key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_outdentLineCodeblock - .tr(); - } else if (key == tabSpacesAtCurosrInCodeBlockCommand('').key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_twoSpacesCursorCodeblock - .tr(); - } else if (key == customCopyCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_copy.tr(); - } else if (key == customPasteCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_paste.tr(); - } else if (key == customCutCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_cut.tr(); - } else if (key == customTextLeftAlignCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_alignLeft.tr(); - } else if (key == customTextCenterAlignCommand.key) { - 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) { - label = LocaleKeys.settings_shortcutsPage_keybindings_redo.tr(); - } else if (key == convertToParagraphCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_convertToParagraph.tr(); - } else if (key == backspaceCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr(); - } else if (key == deleteLeftWordCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_deleteLeftWord.tr(); - } else if (key == deleteLeftSentenceCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_deleteLeftSentence.tr(); - } else if (key == deleteCommand.key) { - label = UniversalPlatform.isMacOS - ? LocaleKeys.settings_shortcutsPage_keybindings_deleteMacOS.tr() - : LocaleKeys.settings_shortcutsPage_keybindings_delete.tr(); - } else if (key == deleteRightWordCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_deleteRightWord.tr(); - } else if (key == moveCursorLeftCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeft.tr(); - } else if (key == moveCursorToBeginCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorBeginning - .tr(); - } else if (key == moveCursorToLeftWordCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeftWord.tr(); - } else if (key == moveCursorLeftSelectCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeftSelect - .tr(); - } else if (key == moveCursorBeginSelectCommand.key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_moveCursorBeginSelect - .tr(); - } else if (key == moveCursorLeftWordSelectCommand.key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_moveCursorLeftWordSelect - .tr(); - } else if (key == moveCursorRightCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_moveCursorRight.tr(); - } else if (key == moveCursorToEndCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorEnd.tr(); - } else if (key == moveCursorToRightWordCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorRightWord - .tr(); - } else if (key == moveCursorRightSelectCommand.key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_moveCursorRightSelect - .tr(); - } else if (key == moveCursorEndSelectCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorEndSelect - .tr(); - } else if (key == moveCursorRightWordSelectCommand.key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_moveCursorRightWordSelect - .tr(); - } else if (key == moveCursorUpCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorUp.tr(); - } else if (key == moveCursorTopSelectCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorTopSelect - .tr(); - } else if (key == moveCursorTopCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorTop.tr(); - } else if (key == moveCursorUpSelectCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_moveCursorUpSelect.tr(); - } else if (key == moveCursorDownCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorDown.tr(); - } else if (key == moveCursorBottomSelectCommand.key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_moveCursorBottomSelect - .tr(); - } else if (key == moveCursorBottomCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_moveCursorBottom.tr(); - } else if (key == moveCursorDownSelectCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorDownSelect - .tr(); - } else if (key == homeCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_home.tr(); - } else if (key == endCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_end.tr(); - } else if (key == toggleBoldCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_toggleBold.tr(); - } else if (key == toggleItalicCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_toggleItalic.tr(); - } else if (key == toggleUnderlineCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_toggleUnderline.tr(); - } else if (key == toggleStrikethroughCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_toggleStrikethrough - .tr(); - } else if (key == toggleCodeCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_toggleCode.tr(); - } else if (key == toggleHighlightCommand.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_toggleHighlight.tr(); - } else if (key == showLinkMenuCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_showLinkMenu.tr(); - } else if (key == openInlineLinkCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_openInlineLink.tr(); - } else if (key == openLinksCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_openLinks.tr(); - } else if (key == indentCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_indent.tr(); - } else if (key == outdentCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_outdent.tr(); - } else if (key == exitEditingCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_exit.tr(); - } else if (key == pageUpCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_pageUp.tr(); - } else if (key == pageDownCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_pageDown.tr(); - } else if (key == selectAllCommand.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_selectAll.tr(); - } else if (key == pasteTextWithoutFormattingCommand.key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_pasteWithoutFormatting - .tr(); - } else if (key == emojiShortcutEvent.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_showEmojiPicker.tr(); - } else if (key == enterInTableCell.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_enterInTableCell.tr(); - } else if (key == leftInTableCell.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_leftInTableCell.tr(); - } else if (key == rightInTableCell.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_rightInTableCell.tr(); - } else if (key == upInTableCell.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_upInTableCell.tr(); - } else if (key == downInTableCell.key) { - label = - LocaleKeys.settings_shortcutsPage_keybindings_downInTableCell.tr(); - } else if (key == tabInTableCell.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_tabInTableCell.tr(); - } else if (key == shiftTabInTableCell.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_shiftTabInTableCell - .tr(); - } else if (key == backSpaceInTableCell.key) { - label = LocaleKeys.settings_shortcutsPage_keybindings_backSpaceInTableCell - .tr(); - } - - return label ?? description?.capitalize() ?? ''; - } -} 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 deleted file mode 100644 index 78ffd34eef..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ /dev/null @@ -1,1376 +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/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'; -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/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/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_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_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/language.dart'; -import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; -import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart'; -import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; -import 'package:flowy_infra/size.dart'; -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:flutter_bloc/flutter_bloc.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class SettingsWorkspaceView extends StatelessWidget { - const SettingsWorkspaceView({ - super.key, - required this.userProfile, - this.currentWorkspaceMemberRole, - }); - - final UserProfilePB userProfile; - final AFRolePB? currentWorkspaceMemberRole; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => WorkspaceSettingsBloc() - ..add(WorkspaceSettingsEvent.initial(userProfile: userProfile)), - child: BlocConsumer( - listener: (context, state) { - if (state.deleteWorkspace) { - context.read().add( - UserWorkspaceEvent.deleteWorkspace( - state.workspace!.workspaceId, - ), - ); - Navigator.of(context).pop(); - } - if (state.leaveWorkspace) { - context.read().add( - UserWorkspaceEvent.leaveWorkspace( - state.workspace!.workspaceId, - ), - ); - Navigator.of(context).pop(); - } - }, - builder: (context, state) { - 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) ...[ - SettingsCategory( - title: LocaleKeys.settings_workspacePage_workspaceName_title - .tr(), - children: [ - _WorkspaceNameSetting( - currentWorkspaceMemberRole: currentWorkspaceMemberRole, - ), - ], - ), - const SettingsCategorySpacer(), - SettingsCategory( - title: LocaleKeys.settings_workspacePage_workspaceIcon_title - .tr(), - description: LocaleKeys - .settings_workspacePage_workspaceIcon_description - .tr(), - children: [ - _WorkspaceIconSetting( - enableEdit: currentWorkspaceMemberRole?.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: - LocaleKeys.settings_workspacePage_theme_description.tr(), - children: const [ - _ThemeDropdown(), - _DocumentCursorColorSetting(), - _DocumentSelectionColorSetting(), - DocumentPaddingSetting(), - ], - ), - const SettingsCategorySpacer(), - SettingsCategory( - title: - LocaleKeys.settings_workspacePage_workspaceFont_title.tr(), - children: [ - _FontSelectorDropdown( - currentFont: - context.read().state.font, - ), - SettingsDashedDivider( - color: Theme.of(context).colorScheme.outline, - ), - SettingsCategory( - title: LocaleKeys.settings_workspacePage_textDirection_title - .tr(), - children: const [ - TextDirectionSelect(), - EnableRTLItemsSwitcher(), - ], - ), - ], - ), - 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: [ - const _DateTimeFormatLabel(), - const _TimeFormatSwitcher(), - SettingsDashedDivider( - color: Theme.of(context).colorScheme.outline, - ), - const _DateFormatDropdown(), - ], - ), - const SettingsCategorySpacer(), - - SettingsCategory( - title: LocaleKeys.settings_workspacePage_language_title.tr(), - children: const [LanguageDropdown()], - ), - const SettingsCategorySpacer(), - - if (userProfile.workspaceAuthType != AuthTypePB.Local) ...[ - SingleSettingAction( - label: LocaleKeys.settings_workspacePage_manageWorkspace_title - .tr(), - fontSize: 16, - fontWeight: FontWeight.w600, - onPressed: () => showConfirmDialog( - context: context, - title: currentWorkspaceMemberRole?.isOwner ?? false - ? LocaleKeys - .settings_workspacePage_deleteWorkspacePrompt_title - .tr() - : LocaleKeys - .settings_workspacePage_leaveWorkspacePrompt_title - .tr(), - description: currentWorkspaceMemberRole?.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 - ? LocaleKeys - .settings_workspacePage_manageWorkspace_deleteWorkspace - .tr() - : LocaleKeys - .settings_workspacePage_manageWorkspace_leaveWorkspace - .tr(), - ), - ], - ], - ); - }, - ), - ); - } -} - -class _WorkspaceNameSetting extends StatefulWidget { - const _WorkspaceNameSetting({ - this.currentWorkspaceMemberRole, - }); - - final AFRolePB? currentWorkspaceMemberRole; - - @override - State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState(); -} - -class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> { - final TextEditingController workspaceNameController = TextEditingController(); - final focusNode = FocusNode(); - Timer? _debounce; - - @override - void dispose() { - focusNode.dispose(); - workspaceNameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (_, state) { - final newName = state.workspace?.name; - if (newName != null && newName != workspaceNameController.text) { - workspaceNameController.text = newName; - } - }, - builder: (_, state) { - if (widget.currentWorkspaceMemberRole == null || - !widget.currentWorkspaceMemberRole!.isOwner) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2.5), - child: FlowyText.regular( - workspaceNameController.text, - fontSize: 14, - ), - ); - } - - return Flexible( - child: SettingsInputField( - textController: workspaceNameController, - value: workspaceNameController.text, - focusNode: focusNode, - onSave: (_) => - _saveWorkspaceName(name: workspaceNameController.text), - onChanged: _debounceSaveName, - hideActions: true, - ), - ); - }, - ); - } - - void _debounceSaveName(String name) { - _debounce?.cancel(); - _debounce = Timer( - const Duration(milliseconds: 300), - () => _saveWorkspaceName(name: name), - ); - } - - void _saveWorkspaceName({required String name}) { - if (name.isNotEmpty) { - context - .read() - .add(WorkspaceSettingsEvent.updateWorkspaceName(name)); - } - } -} - -@visibleForTesting -class LanguageDropdown extends StatelessWidget { - const LanguageDropdown({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return SettingsDropdown( - key: const Key('LanguageDropdown'), - expandWidth: false, - onChanged: (locale) => context - .read() - .setLocale(context, locale), - selectedOption: state.locale, - options: EasyLocalization.of(context)! - .supportedLocales - .map( - (locale) => buildDropdownMenuEntry( - context, - selectedValue: state.locale, - value: locale, - label: languageFromLocale(locale), - ), - ) - .toList(), - ); - }, - ); - } -} - -class _WorkspaceIconSetting extends StatelessWidget { - const _WorkspaceIconSetting({required this.enableEdit, this.workspace}); - - final bool enableEdit; - final UserWorkspacePB? workspace; - - @override - Widget build(BuildContext context) { - if (workspace == null) { - return const SizedBox( - height: 64, - width: 64, - child: CircularProgressIndicator(), - ); - } - - return SizedBox( - height: 64, - width: 64, - 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, - enableEdit: true, - onSelected: (r) => context - .read() - .add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)), - ), - ), - ); - } -} - -@visibleForTesting -class TextDirectionSelect extends StatelessWidget { - const TextDirectionSelect({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final selectedItem = state.textDirection; - - return SettingsRadioSelect( - onChanged: (item) { - context - .read() - .setTextDirection(item.value); - context - .read() - .syncDefaultTextDirection(item.value.name); - }, - items: [ - SettingsRadioItem( - value: AppFlowyTextDirection.ltr, - icon: const FlowySvg(FlowySvgs.textdirection_ltr_m), - label: LocaleKeys.settings_workspacePage_textDirection_leftToRight - .tr(), - isSelected: selectedItem == AppFlowyTextDirection.ltr, - ), - SettingsRadioItem( - value: AppFlowyTextDirection.rtl, - icon: const FlowySvg(FlowySvgs.textdirection_rtl_m), - label: LocaleKeys.settings_workspacePage_textDirection_rightToLeft - .tr(), - isSelected: selectedItem == AppFlowyTextDirection.rtl, - ), - SettingsRadioItem( - value: AppFlowyTextDirection.auto, - icon: const FlowySvg(FlowySvgs.textdirection_auto_m), - label: LocaleKeys.settings_workspacePage_textDirection_auto.tr(), - isSelected: selectedItem == AppFlowyTextDirection.auto, - ), - ], - ); - }, - ); - } -} - -@visibleForTesting -class EnableRTLItemsSwitcher extends StatelessWidget { - const EnableRTLItemsSwitcher({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: FlowyText.regular( - LocaleKeys.settings_workspacePage_textDirection_enableRTLItems.tr(), - fontSize: 16, - ), - ), - const HSpace(16), - Toggle( - value: context - .watch() - .state - .enableRtlToolbarItems, - onChanged: (value) => context - .read() - .setEnableRTLToolbarItems(value), - ), - ], - ); - } -} - -class _LayoutDirectionSelect extends StatelessWidget { - const _LayoutDirectionSelect(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return SettingsRadioSelect( - onChanged: (item) => context - .read() - .setLayoutDirection(item.value), - items: [ - SettingsRadioItem( - value: LayoutDirection.ltrLayout, - icon: const FlowySvg(FlowySvgs.textdirection_ltr_m), - label: LocaleKeys - .settings_workspacePage_layoutDirection_leftToRight - .tr(), - isSelected: state.layoutDirection == LayoutDirection.ltrLayout, - ), - SettingsRadioItem( - value: LayoutDirection.rtlLayout, - icon: const FlowySvg(FlowySvgs.textdirection_rtl_m), - label: LocaleKeys - .settings_workspacePage_layoutDirection_rightToLeft - .tr(), - isSelected: state.layoutDirection == LayoutDirection.rtlLayout, - ), - ], - ); - }, - ); - } -} - -class _DateFormatDropdown extends StatelessWidget { - const _DateFormatDropdown(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.regular( - LocaleKeys.settings_workspacePage_dateTime_dateFormat_label - .tr(), - fontSize: 16, - ), - const VSpace(8), - SettingsDropdown( - key: const Key('DateFormatDropdown'), - expandWidth: false, - onChanged: (format) => context - .read() - .setDateFormat(format), - selectedOption: state.dateFormat, - options: UserDateFormatPB.values - .map( - (format) => buildDropdownMenuEntry( - context, - value: format, - label: _formatLabel(format), - ), - ) - .toList(), - ), - ], - ), - ); - }, - ); - } - - String _formatLabel(UserDateFormatPB format) => switch (format) { - UserDateFormatPB.Locally => - LocaleKeys.settings_workspacePage_dateTime_dateFormat_local.tr(), - UserDateFormatPB.US => - LocaleKeys.settings_workspacePage_dateTime_dateFormat_us.tr(), - UserDateFormatPB.ISO => - LocaleKeys.settings_workspacePage_dateTime_dateFormat_iso.tr(), - UserDateFormatPB.Friendly => - LocaleKeys.settings_workspacePage_dateTime_dateFormat_friendly.tr(), - UserDateFormatPB.DayMonthYear => - LocaleKeys.settings_workspacePage_dateTime_dateFormat_dmy.tr(), - _ => "Unknown format", - }; -} - -class _DateTimeFormatLabel extends StatelessWidget { - const _DateTimeFormatLabel(); - - @override - Widget build(BuildContext context) { - final now = DateTime.now(); - - return BlocBuilder( - builder: (context, state) { - return FlowyText.regular( - LocaleKeys.settings_workspacePage_dateTime_example.tr( - args: [ - state.dateFormat.formatDate(now, false), - state.timeFormat.formatTime(now), - now.timeZoneName, - ], - ), - maxLines: 2, - fontSize: 16, - color: AFThemeExtension.of(context).secondaryTextColor, - ); - }, - ); - } -} - -class _TimeFormatSwitcher extends StatelessWidget { - const _TimeFormatSwitcher(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: FlowyText.regular( - LocaleKeys.settings_workspacePage_dateTime_24HourTime.tr(), - fontSize: 16, - ), - ), - const HSpace(16), - Toggle( - value: context.watch().state.timeFormat == - UserTimeFormatPB.TwentyFourHour, - onChanged: (value) => - context.read().setTimeFormat( - value - ? UserTimeFormatPB.TwentyFourHour - : UserTimeFormatPB.TwelveHour, - ), - ), - ], - ); - } -} - -class _ThemeDropdown extends StatelessWidget { - const _ThemeDropdown(); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DynamicPluginBloc()..add(DynamicPluginEvent.load()), - child: BlocBuilder( - buildWhen: (_, current) => current is Ready, - builder: (context, state) { - final appearance = context.watch().state; - final isLightMode = Theme.of(context).brightness == Brightness.light; - - final customThemes = state.whenOrNull( - ready: (ps) => ps.map((p) => p.theme).whereType(), - ); - - return SettingsDropdown( - key: const Key('ThemeSelectorDropdown'), - actions: [ - SettingAction( - tooltip: LocaleKeys - .settings_workspacePage_theme_uploadCustomThemeTooltip - .tr(), - icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(20)), - onPressed: () => Dialogs.show( - context, - child: BlocProvider.value( - value: context.read(), - child: const FlowyDialog( - constraints: BoxConstraints(maxHeight: 300), - child: ThemeUploadWidget(), - ), - ), - ).then((val) { - if (val != null && context.mounted) { - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_themeUpload_uploadSuccess - .tr(), - ); - } - }), - ), - SettingAction( - icon: const FlowySvg( - FlowySvgs.restore_s, - size: Size.square(20), - ), - label: LocaleKeys.settings_common_reset.tr(), - onPressed: () => context - .read() - .setTheme(AppTheme.builtins.first.themeName), - ), - ], - onChanged: (theme) => - context.read().setTheme(theme), - selectedOption: appearance.appTheme.themeName, - options: [ - ...AppTheme.builtins.map( - (t) { - final theme = isLightMode ? t.lightTheme : t.darkTheme; - - return buildDropdownMenuEntry( - context, - selectedValue: appearance.appTheme.themeName, - value: t.themeName, - label: t.themeName, - leadingWidget: _ThemeLeading(color: theme.sidebarBg), - ); - }, - ), - ...?customThemes?.map( - (t) { - final theme = isLightMode ? t.lightTheme : t.darkTheme; - - return buildDropdownMenuEntry( - context, - selectedValue: appearance.appTheme.themeName, - value: t.themeName, - label: t.themeName, - leadingWidget: _ThemeLeading(color: theme.sidebarBg), - trailingWidget: FlowyIconButton( - icon: const FlowySvg(FlowySvgs.delete_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - onPressed: () { - context.read().add( - DynamicPluginEvent.removePlugin( - name: t.themeName, - ), - ); - - if (appearance.appTheme.themeName == t.themeName) { - context - .read() - .setTheme(AppTheme.builtins.first.themeName); - } - }, - ), - ); - }, - ), - ], - ); - }, - ), - ); - } -} - -class _ThemeLeading extends StatelessWidget { - const _ThemeLeading({required this.color}); - - final Color color; - - @override - Widget build(BuildContext context) { - return Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: color, - borderRadius: Corners.s4Border, - border: Border.all(color: Theme.of(context).colorScheme.outline), - ), - ); - } -} - -@visibleForTesting -class AppearanceSelector extends StatelessWidget { - const AppearanceSelector({super.key}); - - @override - Widget build(BuildContext context) { - final themeMode = context.read().state.themeMode; - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...ThemeMode.values.map( - (t) => Padding( - padding: const EdgeInsets.only(right: 16), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => - context.read().setThemeMode(t), - child: FlowyHover( - style: HoverStyle.transparent( - foregroundColorOnHover: - AFThemeExtension.of(context).textColor, - ), - child: Column( - children: [ - Container( - width: 88, - height: 72, - decoration: BoxDecoration( - border: Border.all( - color: t == themeMode - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.outline, - ), - borderRadius: Corners.s4Border, - image: DecorationImage( - fit: BoxFit.cover, - image: AssetImage( - 'assets/images/appearance/${t.name.toLowerCase()}.png', - ), - ), - ), - child: t != themeMode - ? null - : const _SelectedModeIndicator(), - ), - const VSpace(6), - FlowyText.regular(getLabel(t), textAlign: TextAlign.center), - ], - ), - ), - ), - ), - ), - ], - ); - } - - String getLabel(ThemeMode t) => switch (t) { - ThemeMode.system => - LocaleKeys.settings_workspacePage_appearance_options_system.tr(), - ThemeMode.light => - LocaleKeys.settings_workspacePage_appearance_options_light.tr(), - ThemeMode.dark => - LocaleKeys.settings_workspacePage_appearance_options_dark.tr(), - }; -} - -class _SelectedModeIndicator extends StatelessWidget { - const _SelectedModeIndicator(); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Positioned( - top: 4, - left: 4, - child: Material( - shape: const CircleBorder(), - elevation: 2, - child: Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - ), - height: 16, - width: 16, - child: const FlowySvg( - FlowySvgs.settings_selected_theme_m, - size: Size.square(16), - blendMode: BlendMode.dstIn, - ), - ), - ), - ), - ], - ); - } -} - -class _FontSelectorDropdown extends StatefulWidget { - const _FontSelectorDropdown({required this.currentFont}); - - final String currentFont; - - @override - State<_FontSelectorDropdown> createState() => _FontSelectorDropdownState(); -} - -class _FontSelectorDropdownState extends State<_FontSelectorDropdown> { - late final _options = [defaultFontFamily, ...GoogleFonts.asMap().keys]; - final _focusNode = FocusNode(); - final _controller = PopoverController(); - late final ScrollController _scrollController; - final _textController = TextEditingController(); - - @override - void initState() { - super.initState(); - const itemExtent = 32; - final index = _options.indexOf(widget.currentFont); - final newPosition = (index * itemExtent).toDouble(); - _scrollController = ScrollController(initialScrollOffset: newPosition); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _textController.text = context - .read() - .state - .font - .fontFamilyDisplayName; - }); - } - - @override - void dispose() { - _controller.close(); - _focusNode.dispose(); - _scrollController.dispose(); - _textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final appearance = context.watch().state; - return LayoutBuilder( - builder: (context, constraints) => AppFlowyPopover( - margin: EdgeInsets.zero, - controller: _controller, - skipTraversal: true, - triggerActions: PopoverTriggerFlags.none, - onClose: () { - _focusNode.unfocus(); - setState(() {}); - }, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints( - maxHeight: 150, - maxWidth: constraints.maxWidth - 90, - ), - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - popupBuilder: (_) => _FontListPopup( - currentFont: appearance.font, - scrollController: _scrollController, - controller: _controller, - options: _options, - textController: _textController, - focusNode: _focusNode, - ), - child: Row( - children: [ - Expanded( - child: TapRegion( - behavior: HitTestBehavior.translucent, - onTapOutside: (_) { - _focusNode.unfocus(); - setState(() {}); - }, - child: Listener( - onPointerDown: (_) { - _focusNode.requestFocus(); - setState(() {}); - _controller.show(); - }, - child: FlowyTextField( - autoFocus: false, - focusNode: _focusNode, - controller: _textController, - decoration: InputDecoration( - suffixIcon: const MouseRegion( - cursor: SystemMouseCursors.click, - child: Icon(Icons.arrow_drop_down), - ), - counterText: '', - contentPadding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 18, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: Corners.s8Border, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: Corners.s8Border, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: Corners.s8Border, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: Corners.s8Border, - ), - ), - ), - ), - ), - ), - const HSpace(16), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => context - .read() - .setFontFamily(defaultFontFamily), - child: SizedBox( - height: 26, - child: FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.restore_s, - size: Size.square(20), - ), - const HSpace(4), - FlowyText.regular( - LocaleKeys.settings_common_reset.tr(), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -class _FontListPopup extends StatefulWidget { - const _FontListPopup({ - required this.controller, - required this.scrollController, - required this.options, - required this.currentFont, - required this.textController, - required this.focusNode, - }); - - final ScrollController scrollController; - final List options; - final String currentFont; - final TextEditingController textController; - final FocusNode focusNode; - final PopoverController controller; - - @override - State<_FontListPopup> createState() => _FontListPopupState(); -} - -class _FontListPopupState extends State<_FontListPopup> { - late List _filteredOptions = widget.options; - - @override - void initState() { - super.initState(); - widget.textController.addListener(_onTextFieldChanged); - } - - void _onTextFieldChanged() { - final value = widget.textController.text; - - if (value.trim().isEmpty) { - _filteredOptions = widget.options; - } else { - if (value.fontFamilyDisplayName == - widget.currentFont.fontFamilyDisplayName) { - return; - } - - _filteredOptions = widget.options - .where( - (f) => - f.toLowerCase().contains(value.trim().toLowerCase()) || - f.fontFamilyDisplayName - .toLowerCase() - .contains(value.trim().fontFamilyDisplayName.toLowerCase()), - ) - .toList(); - - // Default font family is "", but the display name is "System", - // which means it's hard compared to other font families to find this one. - if (!_filteredOptions.contains(defaultFontFamily) && - 'system'.contains(value.trim().toLowerCase())) { - _filteredOptions.insert(0, defaultFontFamily); - } - } - - setState(() {}); - } - - @override - void dispose() { - widget.textController.removeListener(_onTextFieldChanged); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Material( - type: MaterialType.transparency, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_filteredOptions.isEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: FlowyText.medium( - LocaleKeys.settings_workspacePage_workspaceFont_noFontHint.tr(), - ), - ), - Flexible( - child: ListView.separated( - shrinkWrap: _filteredOptions.length < 10, - controller: widget.scrollController, - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), - itemCount: _filteredOptions.length, - separatorBuilder: (_, __) => const VSpace(6), - itemBuilder: (context, index) { - final font = _filteredOptions[index]; - final isSelected = widget.currentFont == font; - return SizedBox( - height: 29, - 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, - onTap: () { - context - .read() - .setFontFamily(font); - - widget.textController.text = font.fontFamilyDisplayName; - - // This is a workaround such that when dialog rebuilds due - // to font changing, the font selector won't retain focus. - widget.focusNode.parent?.requestFocus(); - - widget.controller.close(); - }, - title: Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - font.fontFamilyDisplayName, - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontFamily: getGoogleFontSafely(font).fontFamily, - ), - ), - ), - trailing: - isSelected ? const FlowySvg(FlowySvgs.check_s) : null, - ), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _DocumentCursorColorSetting extends StatelessWidget { - const _DocumentCursorColorSetting(); - - @override - Widget build(BuildContext context) { - final label = - LocaleKeys.settings_appearance_documentSettings_cursorColor.tr(); - return BlocBuilder( - builder: (context, state) { - 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), - ); - }, - trailing: [ - DocumentColorSettingButton( - key: const Key('DocumentCursorColorSettingButton'), - currentColor: state.cursorColor ?? - DefaultAppearanceSettings.getDefaultCursorColor(context), - previewWidgetBuilder: (color) => _CursorColorValueWidget( - cursorColor: color ?? - DefaultAppearanceSettings.getDefaultCursorColor(context), - ), - dialogTitle: label, - onApply: (color) => context - ..read().setDocumentCursorColor(color) - ..read().syncCursorColor(color), - ), - ], - ); - }, - ); - } -} - -class _CursorColorValueWidget extends StatelessWidget { - const _CursorColorValueWidget({required this.cursorColor}); - - final Color cursorColor; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container(color: cursorColor, width: 2, height: 16), - FlowyText( - LocaleKeys.appName.tr(), - // To avoid the text color changes when it is hovered in dark mode - color: AFThemeExtension.of(context).onBackground, - ), - ], - ); - } -} - -class _DocumentSelectionColorSetting extends StatelessWidget { - const _DocumentSelectionColorSetting(); - - @override - Widget build(BuildContext context) { - final label = - LocaleKeys.settings_appearance_documentSettings_selectionColor.tr(); - - return BlocBuilder( - builder: (context, state) { - 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), - ); - }, - trailing: [ - DocumentColorSettingButton( - currentColor: state.selectionColor ?? - DefaultAppearanceSettings.getDefaultSelectionColor(context), - previewWidgetBuilder: (color) => _SelectionColorValueWidget( - selectionColor: color ?? - DefaultAppearanceSettings.getDefaultSelectionColor(context), - ), - dialogTitle: label, - onApply: (c) => context - ..read().setDocumentSelectionColor(c) - ..read().syncSelectionColor(c), - ), - ], - ); - }, - ); - } -} - -class _SelectionColorValueWidget extends StatelessWidget { - const _SelectionColorValueWidget({required this.selectionColor}); - - final Color selectionColor; - - @override - Widget build(BuildContext context) { - // To avoid the text color changes when it is hovered in dark mode - final textColor = AFThemeExtension.of(context).onBackground; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - color: selectionColor, - child: FlowyText( - LocaleKeys.settings_appearance_documentSettings_app.tr(), - color: textColor, - ), - ), - FlowyText( - LocaleKeys.settings_appearance_documentSettings_flowy.tr(), - color: textColor, - ), - ], - ); - } -} - -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..09e719c2e4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,110 +1,72 @@ -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: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/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.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/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:easy_localization/easy_localization.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'); +const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12); +const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0); 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; + SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id)); @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), + title: Padding( + padding: _dialogHorizontalPadding + _contentInsetPadding, + child: FlowyText( + LocaleKeys.settings_title.tr(), + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.tertiary, + ), + ), child: ScaffoldMessenger( child: Scaffold( backgroundColor: Colors.transparent, - body: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: SettingsMenu( - userProfile: user, - changeSelectedPage: (index) => context - .read() - .add(SettingsDialogEvent.setSelectedPage(index)), - currentPage: - context.read().state.page, - isBillingEnabled: state.isBillingEnabled, + body: Padding( + padding: _dialogHorizontalPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: SettingsMenu( + changeSelectedPage: (index) { + context + .read() + .add(SettingsDialogEvent.setSelectedPage(index)); + }, + currentPage: + context.read().state.page, + ), ), - ), - Expanded( - child: getSettingsView( - context - .read() - .state - .currentWorkspace! - .workspaceId, - context.read().state.page, - context.read().state.userProfile, - context - .read() - .state - .currentWorkspace - ?.role, + VerticalDivider( + color: Theme.of(context).dividerColor, ), - ), - ], + const SizedBox(width: 10), + Expanded( + child: getSettingsView( + context.read().state.page, + context.read().state.userProfile, + ), + ) + ], + ), ), ), ), @@ -113,484 +75,18 @@ class SettingsDialog extends StatelessWidget { ); } - Widget getSettingsView( - String workspaceId, - SettingsPage page, - UserProfilePB user, - AFRolePB? currentWorkspaceMemberRole, - ) { + Widget getSettingsView(SettingsPage page, UserProfilePB user) { switch (page) { - case SettingsPage.account: - return SettingsAccountView( - userProfile: user, - didLogout: didLogout, - didLogin: dismissDialog, - ); - case SettingsPage.workspace: - return SettingsWorkspaceView( - userProfile: user, - currentWorkspaceMemberRole: currentWorkspaceMemberRole, - ); - case SettingsPage.manageData: - return SettingsManageDataView(userProfile: user); - case SettingsPage.notifications: - return const SettingsNotificationsView(); - case SettingsPage.cloud: - 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, - ); - case SettingsPage.plan: - return SettingsPlanView( - workspaceId: workspaceId, - user: user, - ); - case SettingsPage.billing: - return SettingsBillingView( - workspaceId: workspaceId, - user: user, - ); - case SettingsPage.sites: - return SettingsSitesPage( - 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(); + case SettingsPage.appearance: + return const SettingsAppearanceView(); + case SettingsPage.language: + return const SettingsLanguageView(); + case SettingsPage.files: + return const SettingsFileSystemView(); + case SettingsPage.user: + return SettingsUserView(user); default: - throw Exception('Unsupported server type: $this'); + return Container(); } } } - -@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 deleted file mode 100644 index 720f7793f2..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -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( - foregroundColor: - WidgetStatePropertyAll(Theme.of(context).colorScheme.primary), - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - ), - minimumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), - maximumSize: WidgetStatePropertyAll(Size(double.infinity, maximumHeight)), - ), - value: value, - label: label, - leadingIcon: leadingWidget, - labelWidget: labelWidget, - trailingIcon: Row( - children: [ - if (trailingWidget != null) ...[ - trailingWidget, - const HSpace(8), - ], - value == selectedValue - ? const FlowySvg(FlowySvgs.check_s) - : const SizedBox.shrink(), - ], - ), - ); -} 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 deleted file mode 100644 index 9892fd18a8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart +++ /dev/null @@ -1,429 +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: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'; - -class DocumentColorSettingButton extends StatefulWidget { - const DocumentColorSettingButton({ - super.key, - required this.currentColor, - required this.previewWidgetBuilder, - required this.dialogTitle, - required this.onApply, - }); - - /// current color from backend - final Color currentColor; - - /// Build a preview widget with the given color - /// It shows both on the [DocumentColorSettingButton] and [_DocumentColorSettingDialog] - final Widget Function(Color? color) previewWidgetBuilder; - - final String dialogTitle; - - final void Function(Color selectedColorOnDialog) onApply; - - @override - State createState() => - _DocumentColorSettingButtonState(); -} - -class _DocumentColorSettingButtonState - extends State { - late Color newColor = widget.currentColor; - - @override - Widget build(BuildContext context) { - return FlowyButton( - margin: const EdgeInsets.all(8), - text: widget.previewWidgetBuilder.call(widget.currentColor), - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - expandText: false, - onTap: () => SettingsAlertDialog( - title: widget.dialogTitle, - confirm: () { - widget.onApply(newColor); - Navigator.of(context).pop(); - }, - children: [ - _DocumentColorSettingDialog( - formKey: GlobalKey(), - currentColor: widget.currentColor, - previewWidgetBuilder: widget.previewWidgetBuilder, - onChanged: (color) => newColor = color, - ), - ], - ).show(context), - ); - } -} - -class _DocumentColorSettingDialog extends StatefulWidget { - const _DocumentColorSettingDialog({ - required this.formKey, - required this.currentColor, - required this.previewWidgetBuilder, - required this.onChanged, - }); - - final GlobalKey formKey; - final Color currentColor; - final Widget Function(Color?) previewWidgetBuilder; - final void Function(Color selectedColor) onChanged; - - @override - State<_DocumentColorSettingDialog> createState() => - DocumentColorSettingDialogState(); -} - -class DocumentColorSettingDialogState - extends State<_DocumentColorSettingDialog> { - /// The color displayed in the dialog. - /// It is `null` when the user didn't enter a valid color value. - late Color? selectedColorOnDialog; - late String currentColorHexString; - late TextEditingController hexController; - late TextEditingController opacityController; - - @override - void initState() { - super.initState(); - selectedColorOnDialog = widget.currentColor; - currentColorHexString = ColorExtension(widget.currentColor).toHexString(); - hexController = TextEditingController( - text: currentColorHexString.extractHex(), - ); - opacityController = TextEditingController( - text: currentColorHexString.extractOpacity(), - ); - } - - @override - void dispose() { - hexController.dispose(); - opacityController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - SizedBox( - width: 100, - height: 40, - child: Center( - child: widget.previewWidgetBuilder( - selectedColorOnDialog, - ), - ), - ), - const VSpace(8), - Form( - key: widget.formKey, - child: Column( - children: [ - _ColorSettingTextField( - controller: hexController, - labelText: LocaleKeys.editor_hexValue.tr(), - hintText: '6fc9e7', - 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(), - validator: (value) => validateOpacityValue(value), - ), - ], - ), - ), - ], - ); - } - - 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 { - const _ColorSettingTextField({ - required this.controller, - required this.labelText, - required this.hintText, - required this.onFieldSubmitted, - this.suffixIcon, - this.onChanged, - this.validator, - }); - - final TextEditingController controller; - final String labelText; - final String hintText; - final void Function(String) onFieldSubmitted; - final Widget? suffixIcon; - final void Function(String)? onChanged; - final String? Function(String?)? validator; - - @override - Widget build(BuildContext context) { - final style = Theme.of(context); - return TextFormField( - controller: controller, - decoration: InputDecoration( - labelText: labelText, - hintText: hintText, - suffixIcon: suffixIcon, - border: OutlineInputBorder( - borderSide: BorderSide(color: style.colorScheme.outline), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: style.colorScheme.outline), - ), - ), - style: style.textTheme.bodyMedium, - onChanged: onChanged, - onFieldSubmitted: onFieldSubmitted, - validator: validator, - autovalidateMode: AutovalidateMode.onUserInteraction, - ); - } -} - -String? validateHexValue(String? hexValue, String opacityValue) { - if (hexValue == null || hexValue.isEmpty) { - return LocaleKeys.settings_appearance_documentSettings_hexEmptyError.tr(); - } - if (hexValue.length != 6) { - return LocaleKeys.settings_appearance_documentSettings_hexLengthError.tr(); - } - - if (validateOpacityValue(opacityValue) == null) { - final colorValue = - int.tryParse(hexValue.combineHexWithOpacity(opacityValue)); - - if (colorValue == null) { - return LocaleKeys.settings_appearance_documentSettings_hexInvalidError - .tr(); - } - } - - return null; -} - -String? validateOpacityValue(String? value) { - if (value == null || value.isEmpty) { - return LocaleKeys.settings_appearance_documentSettings_opacityEmptyError - .tr(); - } - - final opacityInt = int.tryParse(value); - if (opacityInt == null || opacityInt > 100 || opacityInt <= 0) { - return LocaleKeys.settings_appearance_documentSettings_opacityRangeError - .tr(); - } - 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 deleted file mode 100644 index 68556f8294..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -class FlowyGradientButton extends StatefulWidget { - const FlowyGradientButton({ - super.key, - required this.label, - this.onPressed, - this.fontWeight = FontWeight.w600, - this.textColor = Colors.white, - this.backgroundColor, - }); - - final String label; - final VoidCallback? onPressed; - final FontWeight fontWeight; - - /// Used to provide a custom foreground color for the button, used in cases - /// where a custom [backgroundColor] is provided and the default text color - /// does not have enough contrast. - /// - final Color textColor; - - /// Used to provide a custom background color for the button, this will - /// override the gradient behavior, and is mostly used in rare cases - /// where the gradient doesn't have contrast with the background. - /// - final Color? backgroundColor; - - @override - State createState() => _FlowyGradientButtonState(); -} - -class _FlowyGradientButtonState extends State { - bool isHovering = false; - - @override - Widget build(BuildContext context) { - return Listener( - onPointerDown: (_) => widget.onPressed?.call(), - child: MouseRegion( - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - cursor: SystemMouseCursors.click, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - blurRadius: 4, - color: Colors.black.withValues(alpha: 0.25), - offset: const Offset(0, 2), - ), - ], - borderRadius: BorderRadius.circular(16), - color: widget.backgroundColor, - gradient: widget.backgroundColor != null - ? null - : LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - isHovering - ? const Color.fromARGB(255, 57, 40, 92) - : const Color(0xFF44326B), - isHovering - ? const Color.fromARGB(255, 96, 53, 164) - : const Color(0xFF7547C0), - ], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: FlowyText( - widget.label, - fontSize: 16, - fontWeight: widget.fontWeight, - color: widget.textColor, - maxLines: 2, - textAlign: TextAlign.center, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart deleted file mode 100644 index e4551d1c2c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.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'; - -class SettingAction extends StatelessWidget { - const SettingAction({ - super.key, - required this.onPressed, - required this.icon, - this.label, - this.tooltip, - }); - - final VoidCallback onPressed; - final Widget icon; - final String? label; - final String? tooltip; - - @override - Widget build(BuildContext context) { - final child = GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onPressed, - child: SizedBox( - height: 26, - child: FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: Row( - children: [ - icon, - if (label != null) ...[ - const HSpace(4), - FlowyText.regular(label!), - ], - ], - ), - ), - ), - ), - ); - - if (tooltip != null) { - return FlowyTooltip( - message: tooltip!, - child: child, - ); - } - - return child; - } -} 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 deleted file mode 100644 index 9c1f0f4fc4..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_list_tile.dart +++ /dev/null @@ -1,91 +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'; - -class SettingListTile extends StatelessWidget { - const SettingListTile({ - super.key, - this.resetTooltipText, - this.resetButtonKey, - required this.label, - this.hint, - this.trailing, - this.subtitle, - this.onResetRequested, - }); - - final String label; - final String? hint; - final String? resetTooltipText; - final Key? resetButtonKey; - final List? trailing; - final List? subtitle; - final VoidCallback? onResetRequested; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - label, - fontSize: 14, - overflow: TextOverflow.ellipsis, - ), - if (hint != null) - Padding( - padding: const EdgeInsets.only(bottom: 4), - child: FlowyText.regular( - hint!, - fontSize: 10, - color: Theme.of(context).hintColor, - ), - ), - if (subtitle != null) ...subtitle!, - ], - ), - ), - if (trailing != null) ...trailing!, - if (onResetRequested != null) - SettingsResetButton( - key: resetButtonKey, - resetTooltipText: resetTooltipText, - onResetRequested: 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 deleted file mode 100644 index 6c8eeb9ae4..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class SettingValueDropDown extends StatefulWidget { - const SettingValueDropDown({ - super.key, - required this.currentValue, - required this.popupBuilder, - this.popoverKey, - this.onClose, - this.child, - this.popoverController, - this.offset, - this.boxConstraints, - this.margin = const EdgeInsets.all(6), - }); - - final String currentValue; - final Key? popoverKey; - final Widget Function(BuildContext) popupBuilder; - final void Function()? onClose; - final Widget? child; - final PopoverController? popoverController; - final Offset? offset; - final BoxConstraints? boxConstraints; - final EdgeInsets margin; - - @override - State createState() => _SettingValueDropDownState(); -} - -class _SettingValueDropDownState extends State { - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - 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, - ), - offset: widget.offset, - onClose: widget.onClose, - child: widget.child ?? - FlowyTextButton( - widget.currentValue, - fontColor: AFThemeExtension.maybeOf(context)?.onBackground, - fillColor: Colors.transparent, - onPressed: () {}, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart deleted file mode 100644 index 6f92696f28..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -class SettingsActionableInput extends StatelessWidget { - const SettingsActionableInput({ - super.key, - required this.controller, - this.focusNode, - this.placeholder, - this.onSave, - this.actions = const [], - }); - - final TextEditingController controller; - final FocusNode? focusNode; - final String? placeholder; - final Function(String)? onSave; - final List actions; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Flexible( - child: SizedBox( - height: 48, - child: FlowyTextField( - controller: controller, - focusNode: focusNode, - hintText: placeholder, - autoFocus: false, - isDense: false, - suffixIconConstraints: - BoxConstraints.tight(const Size(23 + 18, 24)), - textStyle: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - onSubmitted: onSave, - ), - ), - ), - if (actions.isNotEmpty) ...[ - const HSpace(8), - SeparatedRow( - separatorBuilder: () => const HSpace(16), - children: actions, - ), - ], - ], - ); - } -} 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 deleted file mode 100644 index c56b46eae0..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart +++ /dev/null @@ -1,250 +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/size.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({ - super.key, - this.icon, - required this.title, - this.subtitle, - this.children, - this.cancel, - this.confirm, - this.confirmLabel, - this.hideCancelButton = false, - this.isDangerous = false, - this.implyLeading = false, - this.enableConfirmNotifier, - }); - - final Widget? icon; - final String title; - final String? subtitle; - final List? children; - final void Function()? cancel; - final void Function()? confirm; - 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; - - @override - State createState() => _SettingsAlertDialogState(); -} - -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( - maxHeight: 600, - maxWidth: 600, - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.implyLeading) ...[ - 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, - ), - ), - ), - ], - ), - if (widget.icon != null) ...[ - widget.icon!, - const VSpace(16), - ], - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: FlowyText.medium( - widget.title, - fontSize: 22, - color: Theme.of(context).colorScheme.tertiary, - maxLines: null, - ), - ), - ], - ), - if (widget.subtitle?.isNotEmpty ?? false) ...[ - const VSpace(16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: FlowyText.regular( - widget.subtitle!, - fontSize: 16, - color: Theme.of(context).colorScheme.tertiary, - textAlign: TextAlign.center, - maxLines: null, - ), - ), - ], - ), - ], - if (widget.children?.isNotEmpty ?? false) ...[ - const VSpace(16), - ...widget.children!, - ], - if (widget.confirm != null || !widget.hideCancelButton) ...[ - const VSpace(20), - ], - _Actions( - hideCancelButton: widget.hideCancelButton, - confirmLabel: widget.confirmLabel, - cancel: widget.cancel, - confirm: widget.confirm, - isDangerous: widget.isDangerous, - enableConfirm: enableConfirm, - ), - ], - ), - ); - } -} - -class _Actions extends StatelessWidget { - const _Actions({ - required this.hideCancelButton, - this.confirmLabel, - this.cancel, - this.confirm, - this.isDangerous = false, - this.enableConfirm = true, - }); - - final bool hideCancelButton; - final String? confirmLabel; - final VoidCallback? cancel; - final VoidCallback? confirm; - final bool isDangerous; - final bool enableConfirm; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!hideCancelButton) ...[ - SizedBox( - height: 48, - child: PrimaryRoundedButton( - text: LocaleKeys.button_cancel.tr(), - margin: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - fontWeight: FontWeight.w600, - radius: 12.0, - onTap: () { - cancel?.call(); - Navigator.of(context).pop(); - }, - ), - ), - ], - if (confirm != null && !hideCancelButton) ...[ - const HSpace(8), - ], - if (confirm != null) ...[ - SizedBox( - height: 48, - child: FlowyTextButton( - confirmLabel ?? LocaleKeys.button_confirm.tr(), - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - 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, - ), - ), - ], - ], - ); - } -} 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 deleted file mode 100644 index 8091a72684..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart +++ /dev/null @@ -1,45 +0,0 @@ -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'; - -class SettingsBody extends StatelessWidget { - const SettingsBody({ - super.key, - required this.title, - this.description, - this.autoSeparate = true, - required this.children, - }); - - final String title; - final String? description; - final bool autoSeparate; - final List children; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsHeader(title: title, description: description), - Flexible( - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => autoSeparate - ? const SettingsCategorySpacer() - : const SizedBox.shrink(), - 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 deleted file mode 100644 index 33c81b99e8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart +++ /dev/null @@ -1,75 +0,0 @@ -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'; - -/// Renders a simple category taking a title and the list -/// of children (settings) to be rendered. -/// -class SettingsCategory extends StatelessWidget { - const SettingsCategory({ - super.key, - required this.title, - this.description, - this.descriptionColor, - this.tooltip, - this.actions, - required this.children, - }); - - 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( - title, - style: theme.textStyle.heading4.enhanced( - color: theme.textColorScheme.primary, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - if (tooltip != null) ...[ - const HSpace(4), - FlowyTooltip( - message: tooltip, - child: const FlowySvg(FlowySvgs.information_s), - ), - ], - const Spacer(), - if (actions != null) ...actions!, - ], - ), - const VSpace(16), - if (description?.isNotEmpty ?? false) ...[ - FlowyText.regular( - description!, - maxLines: 4, - fontSize: 12, - overflow: TextOverflow.ellipsis, - color: descriptionColor, - ), - const VSpace(8), - ], - SeparatedColumn( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => - children.length > 1 ? const VSpace(16) : const SizedBox.shrink(), - children: children, - ), - ], - ); - } -} 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 deleted file mode 100644 index deec09c1d8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -/// This is used to create a uniform space and divider -/// between categories in settings. -/// -class SettingsCategorySpacer extends StatelessWidget { - const SettingsCategorySpacer({super.key}); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Divider( - height: 32, - color: theme.borderColorScheme.greyPrimary, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dashed_divider.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dashed_divider.dart deleted file mode 100644 index 6a8405dc56..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dashed_divider.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Renders a dashed divider -/// -/// The length of each dash is the same as the gap. -/// -class SettingsDashedDivider extends StatelessWidget { - const SettingsDashedDivider({ - super.key, - this.color, - this.height, - this.strokeWidth = 1.0, - this.gap = 3.0, - this.direction = Axis.horizontal, - }); - - // The color of the divider, defaults to the theme's divider color - final Color? color; - - // The height of the divider, this will surround the divider equally - final double? height; - - // Thickness of the divider - final double strokeWidth; - - // Gap between the dashes - final double gap; - - // Direction of the divider - final Axis direction; - - @override - Widget build(BuildContext context) { - final double padding = - height != null && height! > 0 ? (height! - strokeWidth) / 2 : 0; - - return LayoutBuilder( - builder: (context, constraints) { - final items = _calculateItems(constraints); - return Padding( - padding: EdgeInsets.symmetric( - vertical: direction == Axis.horizontal ? padding : 0, - horizontal: direction == Axis.vertical ? padding : 0, - ), - child: Wrap( - direction: direction, - children: List.generate( - items, - (index) => Container( - margin: EdgeInsets.only( - right: direction == Axis.horizontal ? gap : 0, - bottom: direction == Axis.vertical ? gap : 0, - ), - width: direction == Axis.horizontal ? gap : strokeWidth, - height: direction == Axis.vertical ? gap : strokeWidth, - decoration: BoxDecoration( - color: color ?? Theme.of(context).dividerColor, - borderRadius: BorderRadius.circular(1.0), - ), - ), - ), - ), - ); - }, - ); - } - - int _calculateItems(BoxConstraints constraints) { - final double totalLength = direction == Axis.horizontal - ? constraints.maxWidth - : constraints.maxHeight; - - return (totalLength / (gap * 2)).floor(); - } -} 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 deleted file mode 100644 index e392ed91f0..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ /dev/null @@ -1,121 +0,0 @@ -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'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -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 { - const SettingsDropdown({ - super.key, - required this.selectedOption, - required this.options, - 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; - final bool expandWidth; - - @override - State> createState() => _SettingsDropdownState(); -} - -class _SettingsDropdownState extends State> { - late final TextEditingController controller = TextEditingController( - text: widget.selectedOption is String - ? widget.selectedOption as String - : widget.options - .firstWhereOrNull((e) => e.value == widget.selectedOption) - ?.label ?? - '', - ); - - @override - Widget build(BuildContext context) { - final fontFamily = context.read().state.font; - final fontFamilyUsed = - getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily; - - return Row( - children: [ - Expanded( - child: AFDropdownMenu( - controller: controller, - 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, - ), - menuStyle: MenuStyle( - maximumSize: - const WidgetStatePropertyAll(Size(double.infinity, 250)), - elevation: const WidgetStatePropertyAll(10), - shadowColor: - WidgetStatePropertyAll(Colors.black.withValues(alpha: 0.4)), - backgroundColor: WidgetStatePropertyAll( - Theme.of(context).cardColor, - ), - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 6, vertical: 8), - ), - alignment: Alignment.bottomLeft, - ), - inputDecorationTheme: InputDecorationTheme( - contentPadding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 18, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: Corners.s8Border, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: Corners.s8Border, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: Corners.s8Border, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: Corners.s8Border, - ), - ), - onSelected: (v) async { - v != null ? widget.onChanged?.call(v) : null; - }, - ), - ), - if (widget.actions?.isNotEmpty == true) ...[ - const HSpace(16), - SeparatedRow( - separatorBuilder: () => const HSpace(8), - children: widget.actions!, - ), - ], - ], - ); - } -} 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 deleted file mode 100644 index 7409070ba9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -/// Renders a simple header for the settings view -/// -class SettingsHeader extends StatelessWidget { - const SettingsHeader({super.key, required this.title, this.description}); - - final String title; - final String? description; - - @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 (description?.isNotEmpty == true) ...[ - const VSpace(8), - FlowyText( - description!, - maxLines: 4, - 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 deleted file mode 100644 index d5a81655a5..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart +++ /dev/null @@ -1,179 +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/flowy_infra_ui.dart'; - -/// This is used to describe a settings input field -/// -/// The input will have secondary action of "save" and "cancel" -/// which will only be shown when the input has changed. -/// -/// _Note: The label can overflow and will be ellipsized._ -/// -class SettingsInputField extends StatefulWidget { - const SettingsInputField({ - super.key, - this.label, - this.textController, - this.focusNode, - this.obscureText = false, - this.value, - this.placeholder, - this.tooltip, - this.onSave, - this.onCancel, - this.hideActions = false, - this.onChanged, - }); - - final String? label; - final TextEditingController? textController; - final FocusNode? focusNode; - - /// If true, the input field will be obscured - /// and an option to toggle to show the text will be provided. - /// - final bool obscureText; - - final String? value; - final String? placeholder; - final String? tooltip; - - /// If true the save and cancel options will not show below the - /// input field. - /// - final bool hideActions; - - final void Function(String)? onSave; - - /// The action to be performed when the cancel button is pressed. - /// - /// If null the button will **NOT** be disabled! Instead it will - /// reset the input to the original value. - /// - final void Function()? onCancel; - - final void Function(String)? onChanged; - - @override - State createState() => _SettingsInputFieldState(); -} - -class _SettingsInputFieldState extends State { - late final controller = - widget.textController ?? TextEditingController(text: widget.value); - late final FocusNode focusNode = widget.focusNode ?? FocusNode(); - late bool obscureText = widget.obscureText; - - @override - void dispose() { - if (widget.focusNode == null) { - focusNode.dispose(); - } - if (widget.textController == null) { - controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - children: [ - if (widget.label?.isNotEmpty == true) ...[ - Flexible( - child: FlowyText.medium( - widget.label!, - color: AFThemeExtension.of(context).secondaryTextColor, - ), - ), - ], - if (widget.tooltip != null) ...[ - const HSpace(4), - FlowyTooltip( - message: widget.tooltip, - child: const FlowySvg(FlowySvgs.information_s), - ), - ], - ], - ), - if (widget.label?.isNotEmpty ?? false || widget.tooltip != null) - const VSpace(8), - SizedBox( - height: 48, - child: FlowyTextField( - focusNode: focusNode, - hintText: widget.placeholder, - controller: controller, - autoFocus: false, - obscureText: obscureText, - isDense: false, - suffixIconConstraints: - BoxConstraints.tight(const Size(23 + 18, 24)), - suffixIcon: !widget.obscureText - ? null - : GestureDetector( - onTap: () => setState(() => obscureText = !obscureText), - child: Padding( - padding: const EdgeInsets.only(right: 18), - child: FlowySvg( - obscureText ? FlowySvgs.show_m : FlowySvgs.hide_m, - size: const Size(12, 15), - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - onSubmitted: widget.onSave, - onChanged: (_) { - widget.onChanged?.call(controller.text); - setState(() {}); - }, - ), - ), - if (!widget.hideActions && - ((widget.value == null && controller.text.isNotEmpty) || - widget.value != null && widget.value != controller.text)) ...[ - const VSpace(8), - Row( - children: [ - const Spacer(), - SizedBox( - height: 21, - child: FlowyTextButton( - LocaleKeys.button_save.tr(), - fontWeight: FontWeight.normal, - padding: EdgeInsets.zero, - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - fontColor: AFThemeExtension.of(context).textColor, - onPressed: () => widget.onSave?.call(controller.text), - ), - ), - const HSpace(24), - SizedBox( - height: 21, - child: FlowyTextButton( - LocaleKeys.button_cancel.tr(), - fontWeight: FontWeight.normal, - padding: EdgeInsets.zero, - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - fontColor: AFThemeExtension.of(context).textColor, - onPressed: () { - setState(() => controller.text = widget.value ?? ''); - widget.onCancel?.call(); - }, - ), - ), - ], - ), - ], - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart deleted file mode 100644 index 91d780ceda..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; - -class SettingsRadioItem { - const SettingsRadioItem({ - required this.value, - required this.label, - required this.isSelected, - this.icon, - }); - - final T value; - final String label; - final bool isSelected; - final Widget? icon; -} - -class SettingsRadioSelect extends StatelessWidget { - const SettingsRadioSelect({ - super.key, - required this.items, - required this.onChanged, - this.selectedItem, - }); - - final List> items; - final void Function(SettingsRadioItem) onChanged; - final SettingsRadioItem? selectedItem; - - @override - Widget build(BuildContext context) { - return Wrap( - spacing: 24, - runSpacing: 8, - children: items - .map( - (i) => GestureDetector( - onTap: () => onChanged(i), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 14, - height: 14, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: AFThemeExtension.of(context).textColor, - ), - ), - child: DecoratedBox( - decoration: BoxDecoration( - color: i.isSelected - ? AFThemeExtension.of(context).textColor - : Colors.transparent, - shape: BoxShape.circle, - ), - ), - ), - const HSpace(8), - if (i.icon != null) ...[i.icon!, const HSpace(4)], - FlowyText.regular(i.label, fontSize: 14), - ], - ), - ), - ) - .toList(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart deleted file mode 100644 index 4be6dbee05..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -/// Renders a simple category taking a title and the list -/// of children (settings) to be rendered. -/// -class SettingsSubcategory extends StatelessWidget { - const SettingsSubcategory({ - super.key, - required this.title, - required this.children, - }); - - final String title; - final List children; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - title, - color: AFThemeExtension.of(context).secondaryTextColor, - maxLines: 2, - fontSize: 14, - overflow: TextOverflow.ellipsis, - ), - const VSpace(8), - SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => - children.length > 1 ? const VSpace(16) : const SizedBox.shrink(), - children: children, - ), - ], - ); - } -} 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 deleted file mode 100644 index 6b0c920a04..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart +++ /dev/null @@ -1,167 +0,0 @@ -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 -/// -/// This will render a simple action that takes the title, -/// the button label, and the button action. -/// -/// _Note: The label can overflow and will be ellipsized, -/// unless maxLines is overriden._ -/// -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.fontSize = 14, - this.fontWeight = FontWeight.normal, - this.minWidth, - }); - - final String label; - final String? description; - final int? labelMaxLines; - final String buttonLabel; - - /// The action to be performed when the button is pressed - /// - /// If null the button will be rendered as disabled. - /// - final VoidCallback? onPressed; - - final SingleSettingsButtonType buttonType; - - 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, - ), - ), - ], - ), - ], - ], - ), - ), - const HSpace(24), - ConstrainedBox( - constraints: BoxConstraints( - minWidth: minWidth ?? 0.0, - maxHeight: 32, - minHeight: 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), - fontSize: 12, - isDangerous: buttonType.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 deleted file mode 100644 index d8aa15a944..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart +++ /dev/null @@ -1,76 +0,0 @@ -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: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({ - super.key, - required this.showRestartHint, - required this.onClick, - }); - - final bool showRestartHint; - final VoidCallback onClick; - - @override - Widget build(BuildContext context) { - final List children = [_buildRestartButton(context)]; - if (showRestartHint) { - children.add( - Padding( - padding: const EdgeInsets.only(top: 10), - child: FlowyText( - LocaleKeys.settings_menu_restartAppTip.tr(), - maxLines: null, - ), - ), - ); - } - - return Column(children: children); - } - - Widget _buildRestartButton(BuildContext context) { - if (UniversalPlatform.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, - ), - ), - ], - ); - // 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(), - 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_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart deleted file mode 100644 index 6e1f6e239f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart +++ /dev/null @@ -1,6 +0,0 @@ -export 'emoji_shortcut_event.dart'; -export 'src/emji_picker_config.dart'; -export 'src/emoji_picker.dart'; -export 'src/emoji_picker_builder.dart'; -export 'src/flowy_emoji_picker_config.dart'; -export 'src/models/emoji_model.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 deleted file mode 100644 index 6959f69788..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart +++ /dev/null @@ -1,32 +0,0 @@ -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:flutter/material.dart'; - -final CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent( - key: 'Ctrl + Alt + E to show emoji picker', - command: 'ctrl+alt+e', - macOSCommand: 'cmd+alt+e', - getDescription: () => 'Show an emoji picker', - handler: _emojiShortcutHandler, -); - -CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { - final selection = editorState.selection; - 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) { - return KeyEventResult.ignored; - } - final container = Overlay.of(context); - emojiMenuService = EmojiMenu(editorState: editorState, overlay: container); - emojiMenuService?.show(''); - 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 deleted file mode 100644 index 9a4690f240..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart +++ /dev/null @@ -1,283 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; - -import 'emoji_picker.dart'; -import 'emoji_picker_builder.dart'; -import 'models/emoji_category_models.dart'; -import 'models/emoji_model.dart'; - -class DefaultEmojiPickerView extends EmojiPickerBuilder { - const DefaultEmojiPickerView( - super.config, - super.state, { - super.key, - }); - - @override - DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState(); -} - -class DefaultEmojiPickerViewState extends State - with TickerProviderStateMixin { - PageController? _pageController; - TabController? _tabController; - final TextEditingController _emojiController = TextEditingController(); - final FocusNode _emojiFocusNode = FocusNode(); - EmojiCategoryGroup searchEmojiList = - EmojiCategoryGroup(EmojiCategory.SEARCH, []); - final scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - - int initCategory = widget.state.emojiCategoryGroupList.indexWhere( - (el) => el.category == widget.config.initCategory, - ); - if (initCategory == -1) { - initCategory = 0; - } - _tabController = TabController( - initialIndex: initCategory, - length: widget.state.emojiCategoryGroupList.length, - vsync: this, - ); - _pageController = PageController(initialPage: initCategory); - _emojiFocusNode.requestFocus(); - _emojiController.addListener(_onEmojiChanged); - } - - @override - void dispose() { - _emojiController.removeListener(_onEmojiChanged); - _emojiController.dispose(); - _emojiFocusNode.dispose(); - _pageController?.dispose(); - _tabController?.dispose(); - scrollController.dispose(); - 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!(), - ), - ); - } - - return const SizedBox.shrink(); - } - - bool isEmojiSearching() => - searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final emojiSize = widget.config.getEmojiSize(constraints.maxWidth); - final style = Theme.of(context); - - return Container( - color: widget.config.bgColor, - padding: const EdgeInsets.all(4), - child: Column( - children: [ - const VSpace(4), - // search bar - SizedBox( - height: 32.0, - child: TextField( - controller: _emojiController, - focusNode: _emojiFocusNode, - autofocus: true, - style: style.textTheme.bodyMedium, - cursorColor: style.textTheme.bodyMedium?.color, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(8), - hintText: widget.config.searchHintText, - hintStyle: widget.config.serachHintTextStyle, - enabledBorder: widget.config.serachBarEnableBorder, - focusedBorder: widget.config.serachBarFocusedBorder, - ), - ), - ), - const VSpace(4), - Row( - children: [ - Expanded( - child: TabBar( - labelColor: widget.config.selectedCategoryIconColor, - unselectedLabelColor: widget.config.categoryIconColor, - controller: isEmojiSearching() - ? TabController(length: 1, vsync: this) - : _tabController, - labelPadding: EdgeInsets.zero, - indicatorColor: - widget.config.selectedCategoryIconBackgroundColor, - padding: const EdgeInsets.symmetric(vertical: 4.0), - indicator: BoxDecoration( - border: Border.all(color: Colors.transparent), - borderRadius: BorderRadius.circular(4.0), - color: style.colorScheme.secondary, - ), - onTap: (index) { - _pageController!.animateToPage( - index, - duration: widget.config.tabIndicatorAnimDuration, - curve: Curves.ease, - ); - }, - tabs: isEmojiSearching() - ? [_buildCategory(EmojiCategory.SEARCH, emojiSize)] - : widget.state.emojiCategoryGroupList - .asMap() - .entries - .map( - (item) => _buildCategory( - item.value.category, - emojiSize, - ), - ) - .toList(), - ), - ), - _buildBackspaceButton(), - ], - ), - Flexible( - child: PageView.builder( - itemCount: searchEmojiList.emoji.isNotEmpty - ? 1 - : widget.state.emojiCategoryGroupList.length, - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - final EmojiCategoryGroup emojiCategoryGroup = - isEmojiSearching() - ? searchEmojiList - : widget.state.emojiCategoryGroupList[index]; - return _buildPage(emojiSize, emojiCategoryGroup); - }, - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildCategory(EmojiCategory category, double categorySize) { - return Tab( - height: categorySize, - child: Icon( - widget.config.getIconForCategory(category), - size: categorySize / 1.3, - ), - ); - } - - Widget _buildButtonWidget({ - required VoidCallback onPressed, - required Widget child, - }) { - if (widget.config.buttonMode == ButtonMode.MATERIAL) { - return InkWell(onTap: onPressed, child: child); - } - return GestureDetector(onTap: onPressed, child: child); - } - - Widget _buildPage(double emojiSize, EmojiCategoryGroup emojiCategoryGroup) { - // Display notice if recent has no entries yet - if (emojiCategoryGroup.category == EmojiCategory.RECENT && - emojiCategoryGroup.emoji.isEmpty) { - return _buildNoRecent(); - } else if (emojiCategoryGroup.category == EmojiCategory.SEARCH && - emojiCategoryGroup.emoji.isEmpty) { - return Center(child: Text(widget.config.noEmojiFoundText)); - } - // Build page normally - return ScrollbarListStack( - axis: Axis.vertical, - controller: scrollController, - barSize: 4.0, - scrollbarPadding: const EdgeInsets.symmetric(horizontal: 4.0), - handleColor: widget.config.scrollBarHandleColor, - showTrack: true, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: GridView.builder( - controller: scrollController, - padding: const EdgeInsets.all(0), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.config.emojiNumberPerRow, - mainAxisSpacing: widget.config.verticalSpacing, - crossAxisSpacing: widget.config.horizontalSpacing, - ), - itemCount: emojiCategoryGroup.emoji.length, - itemBuilder: (context, index) { - final item = emojiCategoryGroup.emoji[index]; - return _buildEmoji(emojiSize, emojiCategoryGroup, item); - }, - cacheExtent: 10, - ), - ), - ); - } - - Widget _buildEmoji( - double emojiSize, - EmojiCategoryGroup emojiCategoryGroup, - Emoji emoji, - ) { - return _buildButtonWidget( - onPressed: () { - widget.state.onEmojiSelected(emojiCategoryGroup.category, emoji); - }, - child: FlowyHover( - child: FittedBox( - child: Text( - emoji.emoji, - style: TextStyle(fontSize: emojiSize), - ), - ), - ), - ); - } - - Widget _buildNoRecent() { - return Center( - child: Text( - widget.config.noRecentsText, - style: widget.config.noRecentsStyle, - textAlign: TextAlign.center, - ), - ); - } -} 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 deleted file mode 100644 index f329e9dd1c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart +++ /dev/null @@ -1,92 +0,0 @@ -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'; - -part 'emji_picker_config.freezed.dart'; - -@freezed -class EmojiPickerConfig with _$EmojiPickerConfig { - // private empty constructor is used to make method work in freezed - // https://pub.dev/packages/freezed#adding-getters-and-methods-to-our-models - const EmojiPickerConfig._(); - const factory EmojiPickerConfig({ - @Default(7) int emojiNumberPerRow, - // The maximum size(width and height) of emoji - // It also depaneds on the screen size and emojiNumberPerRow - @Default(32) double emojiSizeMax, - // Vertical spacing between emojis - @Default(0) double verticalSpacing, - // Horizontal spacing between emojis - @Default(0) double horizontalSpacing, - // The initial [EmojiCategory] that will be selected - @Default(EmojiCategory.RECENT) EmojiCategory initCategory, - // The background color of the Widget - @Default(Color(0xFFEBEFF2)) Color? bgColor, - // The color of the category icons - @Default(Colors.grey) Color? categoryIconColor, - // The color of the category icon when selected - @Default(Colors.blue) Color? selectedCategoryIconColor, - // The color of the category indicator - @Default(Colors.blue) Color? selectedCategoryIconBackgroundColor, - // The color of the loading indicator during initialization - @Default(Colors.blue) Color? progressIndicatorColor, - // The color of the backspace icon button - @Default(Colors.blue) Color? backspaceColor, - // Show extra tab with recently used emoji - @Default(true) bool showRecentsTab, - // Limit of recently used emoji that will be saved - @Default(28) int recentsLimit, - @Default('Search emoji') String searchHintText, - TextStyle? serachHintTextStyle, - InputBorder? serachBarEnableBorder, - InputBorder? serachBarFocusedBorder, - // The text to be displayed if no recent emojis to display - @Default('No recent emoji') String noRecentsText, - TextStyle? noRecentsStyle, - // The text to be displayed if no emoji found - @Default('No emoji found') String noEmojiFoundText, - Color? scrollBarHandleColor, - // Duration of tab indicator to animate to next category - @Default(kTabScrollDuration) Duration tabIndicatorAnimDuration, - // Determines the icon to display for each [EmojiCategory] - @Default(EmojiCategoryIcons()) EmojiCategoryIcons emojiCategoryIcons, - // Change between Material and Cupertino button style - @Default(ButtonMode.MATERIAL) ButtonMode buttonMode, - }) = _EmojiPickerConfig; - - /// Get Emoji size based on properties and screen width - double getEmojiSize(double width) { - final maxSize = width / emojiNumberPerRow; - return min(maxSize, emojiSizeMax); - } - - /// Returns the icon for the category - IconData getIconForCategory(EmojiCategory category) { - switch (category) { - case EmojiCategory.RECENT: - return emojiCategoryIcons.recentIcon; - case EmojiCategory.SMILEYS: - return emojiCategoryIcons.smileyIcon; - case EmojiCategory.ANIMALS: - return emojiCategoryIcons.animalIcon; - case EmojiCategory.FOODS: - return emojiCategoryIcons.foodIcon; - case EmojiCategory.TRAVEL: - return emojiCategoryIcons.travelIcon; - case EmojiCategory.ACTIVITIES: - return emojiCategoryIcons.activityIcon; - case EmojiCategory.OBJECTS: - return emojiCategoryIcons.objectIcon; - case EmojiCategory.SYMBOLS: - return emojiCategoryIcons.symbolIcon; - case EmojiCategory.FLAGS: - return emojiCategoryIcons.flagIcon; - case EmojiCategory.SEARCH: - return emojiCategoryIcons.searchIcon; - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart deleted file mode 100644 index 6cf6f23486..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart +++ /dev/null @@ -1,3223 +0,0 @@ -// Copyright information -// File originally from https://github.com/JeffG05/emoji_picker - -// import 'emoji.dart'; - -// final List> temp = [smileys, animals, foods, activities, travel, objects, symbols, flags]; - -// final List emojiSearchList = temp -// .map((element) { -// return element.entries.map((entry) => Emoji(entry.key, entry.value)).toList(); -// }) -// .toList() -// .first; - -/// Map of all possible emojis along with their names in [Category.SMILEYS] -final Map smileys = Map.fromIterables([ - 'Grinning Face', - 'Grinning Face With Big Eyes', - 'Grinning Face With Smiling Eyes', - 'Beaming Face With Smiling Eyes', - 'Grinning Squinting Face', - 'Grinning Face With Sweat', - 'Rolling on the Floor Laughing', - 'Face With Tears of Joy', - 'Slightly Smiling Face', - 'Upside-Down Face', - 'Winking Face', - 'Smiling Face With Smiling Eyes', - 'Smiling Face With Halo', - 'Smiling Face With Hearts', - 'Smiling Face With Heart-Eyes', - 'Star-Struck', - 'Face Blowing a Kiss', - 'Kissing Face', - 'Smiling Face', - 'Kissing Face With Closed Eyes', - 'Kissing Face With Smiling Eyes', - 'Face Savoring Food', - 'Face With Tongue', - 'Winking Face With Tongue', - 'Zany Face', - 'Squinting Face With Tongue', - 'Money-Mouth Face', - 'Hugging Face', - 'Face With Hand Over Mouth', - 'Shushing Face', - 'Thinking Face', - 'Zipper-Mouth Face', - 'Face With Raised Eyebrow', - 'Neutral Face', - 'Expressionless Face', - 'Face Without Mouth', - 'Smirking Face', - 'Unamused Face', - 'Face With Rolling Eyes', - 'Grimacing Face', - 'Lying Face', - 'Relieved Face', - 'Pensive Face', - 'Sleepy Face', - 'Drooling Face', - 'Sleeping Face', - 'Face With Medical Mask', - 'Face With Thermometer', - 'Face With Head-Bandage', - 'Nauseated Face', - 'Face Vomiting', - 'Sneezing Face', - 'Hot Face', - 'Cold Face', - 'Woozy Face', - 'Dizzy Face', - 'Exploding Head', - 'Cowboy Hat Face', - 'Partying Face', - 'Smiling Face With Sunglasses', - 'Nerd Face', - 'Face With Monocle', - 'Confused Face', - 'Worried Face', - 'Slightly Frowning Face', - 'Frowning Face', - 'Face With Open Mouth', - 'Hushed Face', - 'Astonished Face', - 'Flushed Face', - 'Pleading Face', - 'Frowning Face With Open Mouth', - 'Anguished Face', - 'Fearful Face', - 'Anxious Face With Sweat', - 'Sad but Relieved Face', - 'Crying Face', - 'Loudly Crying Face', - 'Face Screaming in Fear', - 'Confounded Face', - 'Persevering Face', - 'Disappointed Face', - 'Downcast Face With Sweat', - 'Weary Face', - 'Tired Face', - 'Face With Steam From Nose', - 'Pouting Face', - 'Angry Face', - 'Face With Symbols on Mouth', - 'Smiling Face With Horns', - 'Angry Face With Horns', - 'Skull', - 'Skull and Crossbones', - 'Pile of Poo', - 'Clown Face', - 'Ogre', - 'Goblin', - 'Ghost', - 'Alien', - 'Alien Monster', - 'Robot Face', - 'Grinning Cat Face', - 'Grinning Cat Face With Smiling Eyes', - 'Cat Face With Tears of Joy', - 'Smiling Cat Face With Heart-Eyes', - 'Cat Face With Wry Smile', - 'Kissing Cat Face', - 'Weary Cat Face', - 'Crying Cat Face', - 'Pouting Cat Face', - 'Kiss Mark', - 'Waving Hand', - 'Raised Back of Hand', - 'Hand With Fingers Splayed', - 'Raised Hand', - 'Vulcan Salute', - 'OK Hand', - 'Victory Hand', - 'Crossed Fingers', - 'Love-You Gesture', - 'Sign of the Horns', - 'Call Me Hand', - 'Backhand Index Pointing Left', - 'Backhand Index Pointing Right', - 'Backhand Index Pointing Up', - 'Middle Finger', - 'Backhand Index Pointing Down', - 'Index Pointing Up', - 'Thumbs Up', - 'Thumbs Down', - 'Raised Fist', - 'Oncoming Fist', - 'Left-Facing Fist', - 'Right-Facing Fist', - 'Clapping Hands', - 'Raising Hands', - 'Open Hands', - 'Palms Up Together', - 'Handshake', - 'Folded Hands', - 'Writing Hand', - 'Nail Polish', - 'Selfie', - 'Flexed Biceps', - 'Leg', - 'Foot', - 'Ear', - 'Nose', - 'Brain', - 'Tooth', - 'Bone', - 'Eyes', - 'Eye', - 'Tongue', - 'Mouth', - 'Baby', - 'Child', - 'Boy', - 'Girl', - 'Person', - 'Man', - 'Man: Beard', - 'Man: Blond Hair', - 'Man: Red Hair', - 'Man: Curly Hair', - 'Man: White Hair', - 'Man: Bald', - 'Woman', - 'Woman: Blond Hair', - 'Woman: Red Hair', - 'Woman: Curly Hair', - 'Woman: White Hair', - 'Woman: Bald', - 'Older Person', - 'Old Man', - 'Old Woman', - 'Man Frowning', - 'Woman Frowning', - 'Man Pouting', - 'Woman Pouting', - 'Man Gesturing No', - 'Woman Gesturing No', - 'Man Gesturing OK', - 'Woman Gesturing OK', - 'Man Tipping Hand', - 'Woman Tipping Hand', - 'Man Raising Hand', - 'Woman Raising Hand', - 'Man Bowing', - 'Woman Bowing', - 'Man Facepalming', - 'Woman Facepalming', - 'Man Shrugging', - 'Woman Shrugging', - 'Man Health Worker', - 'Woman Health Worker', - 'Man Student', - 'Woman Student', - 'Man Teacher', - 'Woman Teacher', - 'Man Judge', - 'Woman Judge', - 'Man Farmer', - 'Woman Farmer', - 'Man Cook', - 'Woman Cook', - 'Man Mechanic', - 'Woman Mechanic', - 'Man Factory Worker', - 'Woman Factory Worker', - 'Man Office Worker', - 'Woman Office Worker', - 'Man Scientist', - 'Woman Scientist', - 'Man Technologist', - 'Woman Technologist', - 'Man Singer', - 'Woman Singer', - 'Man Artist', - 'Woman Artist', - 'Man Pilot', - 'Woman Pilot', - 'Man Astronaut', - 'Woman Astronaut', - 'Man Firefighter', - 'Woman Firefighter', - 'Man Police Officer', - 'Woman Police Officer', - 'Man Detective', - 'Woman Detective', - 'Man Guard', - 'Woman Guard', - 'Man Construction Worker', - 'Woman Construction Worker', - 'Prince', - 'Princess', - 'Man Wearing Turban', - 'Woman Wearing Turban', - 'Man With Chinese Cap', - 'Woman With Headscarf', - 'Man in Tuxedo', - 'Bride With Veil', - 'Pregnant Woman', - 'Breast-Feeding', - 'Baby Angel', - 'Santa Claus', - 'Mrs. Claus', - 'Man Superhero', - 'Woman Superhero', - 'Man Supervillain', - 'Woman Supervillain', - 'Man Mage', - 'Woman Mage', - 'Man Fairy', - 'Woman Fairy', - 'Man Vampire', - 'Woman Vampire', - 'Merman', - 'Mermaid', - 'Man Elf', - 'Woman Elf', - 'Man Genie', - 'Woman Genie', - 'Man Zombie', - 'Woman Zombie', - 'Man Getting Massage', - 'Woman Getting Massage', - 'Man Getting Haircut', - 'Woman Getting Haircut', - 'Man Walking', - 'Woman Walking', - 'Man Running', - 'Woman Running', - 'Woman Dancing', - 'Man Dancing', - 'Man in Suit Levitating', - 'Men With Bunny Ears', - 'Women With Bunny Ears', - 'Man in Steamy Room', - 'Woman in Steamy Room', - 'Person in Lotus Position', - 'Women Holding Hands', - 'Woman and Man Holding Hands', - 'Men Holding Hands', - 'Kiss', - 'Kiss: Man, Man', - 'Kiss: Woman, Woman', - 'Couple With Heart', - 'Couple With Heart: Man, Man', - 'Couple With Heart: Woman, Woman', - 'Family', - 'Family: Man, Woman, Boy', - 'Family: Man, Woman, Girl', - 'Family: Man, Woman, Girl, Boy', - 'Family: Man, Woman, Boy, Boy', - 'Family: Man, Woman, Girl, Girl', - 'Family: Man, Man, Boy', - 'Family: Man, Man, Girl', - 'Family: Man, Man, Girl, Boy', - 'Family: Man, Man, Boy, Boy', - 'Family: Man, Man, Girl, Girl', - 'Family: Woman, Woman, Boy', - 'Family: Woman, Woman, Girl', - 'Family: Woman, Woman, Girl, Boy', - 'Family: Woman, Woman, Boy, Boy', - 'Family: Woman, Woman, Girl, Girl', - 'Family: Man, Boy', - 'Family: Man, Boy, Boy', - 'Family: Man, Girl', - 'Family: Man, Girl, Boy', - 'Family: Man, Girl, Girl', - 'Family: Woman, Boy', - 'Family: Woman, Boy, Boy', - 'Family: Woman, Girl', - 'Family: Woman, Girl, Boy', - 'Family: Woman, Girl, Girl', - 'Speaking Head', - 'Bust in Silhouette', - 'Busts in Silhouette', - 'Footprints', - 'Luggage', - 'Closed Umbrella', - 'Umbrella', - 'Thread', - 'Yarn', - 'Glasses', - 'Sunglasses', - 'Goggles', - 'Lab Coat', - 'Necktie', - 'T-Shirt', - 'Jeans', - 'Scarf', - 'Gloves', - 'Coat', - 'Socks', - 'Dress', - 'Kimono', - 'Bikini', - 'Woman’s Clothes', - 'Purse', - 'Handbag', - 'Clutch Bag', - 'Backpack', - 'Man’s Shoe', - 'Running Shoe', - 'Hiking Boot', - 'Flat Shoe', - 'High-Heeled Shoe', - 'Woman’s Sandal', - 'Woman’s Boot', - 'Crown', - 'Woman’s Hat', - 'Top Hat', - 'Graduation Cap', - 'Billed Cap', - 'Rescue Worker’s Helmet', - 'Lipstick', - 'Ring', - 'Briefcase', -], [ - '😀', - '😃', - '😄', - '😁', - '😆', - '😅', - '🤣', - '😂', - '🙂', - '🙃', - '😉', - '😊', - '😇', - '🥰', - '😍', - '🤩', - '😘', - '😗', - '☺', - '😚', - '😙', - '😋', - '😛', - '😜', - '🤪', - '😝', - '🤑', - '🤗', - '🤭', - '🤫', - '🤔', - '🤐', - '🤨', - '😐', - '😑', - '😶', - '😏', - '😒', - '🙄', - '😬', - '🤥', - '😌', - '😔', - '😪', - '🤤', - '😴', - '😷', - '🤒', - '🤕', - '🤢', - '🤮', - '🤧', - '🥵', - '🥶', - '🥴', - '😵', - '🤯', - '🤠', - '🥳', - '😎', - '🤓', - '🧐', - '😕', - '😟', - '🙁', - '☹️', - '😮', - '😯', - '😲', - '😳', - '🥺', - '😦', - '😧', - '😨', - '😰', - '😥', - '😢', - '😭', - '😱', - '😖', - '😣', - '😞', - '😓', - '😩', - '😫', - '😤', - '😡', - '😠', - '🤬', - '😈', - '👿', - '💀', - '☠', - '💩', - '🤡', - '👹', - '👺', - '👻', - '👽', - '👾', - '🤖', - '😺', - '😸', - '😹', - '😻', - '😼', - '😽', - '🙀', - '😿', - '😾', - '💋', - '👋', - '🤚', - '🖐', - '✋', - '🖖', - '👌', - '✌', - '🤞', - '🤟', - '🤘', - '🤙', - '👈', - '👉', - '👆', - '🖕', - '👇', - '☝', - '👍', - '👎', - '✊', - '👊', - '🤛', - '🤜', - '👏', - '🙌', - '👐', - '🤲', - '🤝', - '🙏', - '✍', - '💅', - '🤳', - '💪', - '🦵', - '🦶', - '👂', - '👃', - '🧠', - '🦷', - '🦴', - '👀', - '👁', - '👅', - '👄', - '👶', - '🧒', - '👦', - '👧', - '🧑', - '👨', - '🧔', - '👱', - '👨‍🦰', - '👨‍🦱', - '👨‍🦳', - '👨‍🦲', - '👩', - '👱', - '👩‍🦰', - '👩‍🦱', - '👩‍🦳', - '👩‍🦲', - '🧓', - '👴', - '👵', - '🙍', - '🙍', - '🙎', - '🙎', - '🙅', - '🙅', - '🙆', - '🙆', - '💁', - '💁', - '🙋', - '🙋', - '🙇', - '🙇', - '🤦', - '🤦', - '🤷', - '🤷', - '👨‍⚕️', - '👩‍⚕️', - '👨‍🎓', - '👩‍🎓', - '👨‍🏫', - '👩‍🏫', - '👨‍⚖️', - '👩‍⚖️', - '👨‍🌾', - '👩‍🌾', - '👨‍🍳', - '👩‍🍳', - '👨‍🔧', - '👩‍🔧', - '👨‍🏭', - '👩‍🏭', - '👨‍💼', - '👩‍💼', - '👨‍🔬', - '👩‍🔬', - '👨‍💻', - '👩‍💻', - '👨‍🎤', - '👩‍🎤', - '👨‍🎨', - '👩‍🎨', - '👨‍✈️', - '👩‍✈️', - '👨‍🚀', - '👩‍🚀', - '👨‍🚒', - '👩‍🚒', - '👮', - '👮', - '🕵️', - '🕵️', - '💂', - '💂', - '👷', - '👷', - '🤴', - '👸', - '👳', - '👳', - '👲', - '🧕', - '🤵', - '👰', - '🤰', - '🤱', - '👼', - '🎅', - '🤶', - '🦸', - '🦸', - '🦹', - '🦹', - '🧙', - '🧙', - '🧚', - '🧚', - '🧛', - '🧛', - '🧜', - '🧜', - '🧝', - '🧝', - '🧞', - '🧞', - '🧟', - '🧟', - '💆', - '💆', - '💇', - '💇', - '🚶', - '🚶', - '🏃', - '🏃', - '💃', - '🕺', - '🕴', - '👯', - '👯', - '🧖', - '🧖', - '🧘', - '👭', - '👫', - '👬', - '💏', - '👨‍❤️‍💋‍👨', - '👩‍❤️‍💋‍👩', - '💑', - '👨‍❤️‍👨', - '👩‍❤️‍👩', - '👪', - '👨‍👩‍👦', - '👨‍👩‍👧', - '👨‍👩‍👧‍👦', - '👨‍👩‍👦‍👦', - '👨‍👩‍👧‍👧', - '👨‍👨‍👦', - '👨‍👨‍👧', - '👨‍👨‍👧‍👦', - '👨‍👨‍👦‍👦', - '👨‍👨‍👧‍👧', - '👩‍👩‍👦', - '👩‍👩‍👧', - '👩‍👩‍👧‍👦', - '👩‍👩‍👦‍👦', - '👩‍👩‍👧‍👧', - '👨‍👦', - '👨‍👦‍👦', - '👨‍👧', - '👨‍👧‍👦', - '👨‍👧‍👧', - '👩‍👦', - '👩‍👦‍👦', - '👩‍👧', - '👩‍👧‍👦', - '👩‍👧‍👧', - '🗣', - '👤', - '👥', - '👣', - '🧳', - '🌂', - '☂', - '🧵', - '🧶', - '👓', - '🕶', - '🥽', - '🥼', - '👔', - '👕', - '👖', - '🧣', - '🧤', - '🧥', - '🧦', - '👗', - '👘', - '👙', - '👚', - '👛', - '👜', - '👝', - '🎒', - '👞', - '👟', - '🥾', - '🥿', - '👠', - '👡', - '👢', - '👑', - '👒', - '🎩', - '🎓', - '🧢', - '⛑', - '💄', - '💍', - '💼', -]); - -/// Map of all possible emojis along with their names in [Category.ANIMALS] -final Map animals = Map.fromIterables([ - 'Dog Face', - 'Cat Face', - 'Mouse Face', - 'Hamster Face', - 'Rabbit Face', - 'Fox Face', - 'Bear Face', - 'Panda Face', - 'Koala Face', - 'Tiger Face', - 'Lion Face', - 'Cow Face', - 'Pig Face', - 'Pig Nose', - 'Frog Face', - 'Monkey Face', - 'See-No-Evil Monkey', - 'Hear-No-Evil Monkey', - 'Speak-No-Evil Monkey', - 'Monkey', - 'Collision', - 'Dizzy', - 'Sweat Droplets', - 'Dashing Away', - 'Gorilla', - 'Dog', - 'Poodle', - 'Wolf Face', - 'Raccoon', - 'Cat', - 'Tiger', - 'Leopard', - 'Horse Face', - 'Horse', - 'Unicorn Face', - 'Zebra', - 'Ox', - 'Water Buffalo', - 'Cow', - 'Pig', - 'Boar', - 'Ram', - 'Ewe', - 'Goat', - 'Camel', - 'Two-Hump Camel', - 'Llama', - 'Giraffe', - 'Elephant', - 'Rhinoceros', - 'Hippopotamus', - 'Mouse', - 'Rat', - 'Rabbit', - 'Chipmunk', - 'Hedgehog', - 'Bat', - 'Kangaroo', - 'Badger', - 'Paw Prints', - 'Turkey', - 'Chicken', - 'Rooster', - 'Hatching Chick', - 'Baby Chick', - 'Front-Facing Baby Chick', - 'Bird', - 'Penguin', - 'Dove', - 'Eagle', - 'Duck', - 'Swan', - 'Owl', - 'Peacock', - 'Parrot', - 'Crocodile', - 'Turtle', - 'Lizard', - 'Snake', - 'Dragon Face', - 'Dragon', - 'Sauropod', - 'T-Rex', - 'Spouting Whale', - 'Whale', - 'Dolphin', - 'Fish', - 'Tropical Fish', - 'Blowfish', - 'Shark', - 'Octopus', - 'Spiral Shell', - 'Snail', - 'Butterfly', - 'Bug', - 'Ant', - 'Honeybee', - 'Lady Beetle', - 'Cricket', - 'Spider', - 'Spider Web', - 'Scorpion', - 'Mosquito', - 'Microbe', - 'Bouquet', - 'Cherry Blossom', - 'White Flower', - 'Rosette', - 'Rose', - 'Wilted Flower', - 'Hibiscus', - 'Sunflower', - 'Blossom', - 'Tulip', - 'Seedling', - 'Evergreen Tree', - 'Deciduous Tree', - 'Palm Tree', - 'Cactus', - 'Sheaf of Rice', - 'Herb', - 'Shamrock', - 'Four Leaf Clover', - 'Maple Leaf', - 'Fallen Leaf', - 'Leaf Fluttering in Wind', - 'Mushroom', - 'Chestnut', - 'Crab', - 'Lobster', - 'Shrimp', - 'Squid', - 'Globe Showing Europe-Africa', - 'Globe Showing Americas', - 'Globe Showing Asia-Australia', - 'Globe With Meridians', - 'New Moon', - 'Waxing Crescent Moon', - 'First Quarter Moon', - 'Waxing Gibbous Moon', - 'Full Moon', - 'Waning Gibbous Moon', - 'Last Quarter Moon', - 'Waning Crescent Moon', - 'Crescent Moon', - 'New Moon Face', - 'First Quarter Moon Face', - 'Last Quarter Moon Face', - 'Sun', - 'Full Moon Face', - 'Sun With Face', - 'Star', - 'Glowing Star', - 'Shooting Star', - 'Cloud', - 'Sun Behind Cloud', - 'Cloud With Lightning and Rain', - 'Sun Behind Small Cloud', - 'Sun Behind Large Cloud', - 'Sun Behind Rain Cloud', - 'Cloud With Rain', - 'Cloud With Snow', - 'Cloud With Lightning', - 'Tornado', - 'Fog', - 'Wind Face', - 'Rainbow', - 'Umbrella', - 'Umbrella With Rain Drops', - 'High Voltage', - 'Snowflake', - 'Snowman Without Snow', - 'Snowman', - 'Comet', - 'Fire', - 'Droplet', - 'Water Wave', - 'Christmas Tree', - 'Sparkles', - 'Tanabata Tree', - 'Pine Decoration', -], [ - '🐶', - '🐱', - '🐭', - '🐹', - '🐰', - '🦊', - '🐻', - '🐼', - '🐨', - '🐯', - '🦁', - '🐮', - '🐷', - '🐽', - '🐸', - '🐵', - '🙈', - '🙉', - '🙊', - '🐒', - '💥', - '💫', - '💦', - '💨', - '🦍', - '🐕', - '🐩', - '🐺', - '🦝', - '🐈', - '🐅', - '🐆', - '🐴', - '🐎', - '🦄', - '🦓', - '🐂', - '🐃', - '🐄', - '🐖', - '🐗', - '🐏', - '🐑', - '🐐', - '🐪', - '🐫', - '🦙', - '🦒', - '🐘', - '🦏', - '🦛', - '🐁', - '🐀', - '🐇', - '🐿', - '🦔', - '🦇', - '🦘', - '🦡', - '🐾', - '🦃', - '🐔', - '🐓', - '🐣', - '🐤', - '🐥', - '🐦', - '🐧', - '🕊', - '🦅', - '🦆', - '🦢', - '🦉', - '🦚', - '🦜', - '🐊', - '🐢', - '🦎', - '🐍', - '🐲', - '🐉', - '🦕', - '🦖', - '🐳', - '🐋', - '🐬', - '🐟', - '🐠', - '🐡', - '🦈', - '🐙', - '🐚', - '🐌', - '🦋', - '🐛', - '🐜', - '🐝', - '🐞', - '🦗', - '🕷', - '🕸', - '🦂', - '🦟', - '🦠', - '💐', - '🌸', - '💮', - '🏵', - '🌹', - '🥀', - '🌺', - '🌻', - '🌼', - '🌷', - '🌱', - '🌲', - '🌳', - '🌴', - '🌵', - '🌾', - '🌿', - '☘', - '🍀', - '🍁', - '🍂', - '🍃', - '🍄', - '🌰', - '🦀', - '🦞', - '🦐', - '🦑', - '🌍', - '🌎', - '🌏', - '🌐', - '🌑', - '🌒', - '🌓', - '🌔', - '🌕', - '🌖', - '🌗', - '🌘', - '🌙', - '🌚', - '🌛', - '🌜', - '☀', - '🌝', - '🌞', - '⭐', - '🌟', - '🌠', - '☁', - '⛅', - '⛈', - '🌤', - '🌥', - '🌦', - '🌧', - '🌨', - '🌩', - '🌪', - '🌫', - '🌬', - '🌈', - '☂', - '☔', - '⚡', - '❄', - '☃', - '⛄', - '☄', - '🔥', - '💧', - '🌊', - '🎄', - '✨', - '🎋', - '🎍', -]); - -/// Map of all possible emojis along with their names in [Category.FOODS] -final Map foods = Map.fromIterables([ - 'Grapes', - 'Melon', - 'Watermelon', - 'Tangerine', - 'Lemon', - 'Banana', - 'Pineapple', - 'Mango', - 'Red Apple', - 'Green Apple', - 'Pear', - 'Peach', - 'Cherries', - 'Strawberry', - 'Kiwi Fruit', - 'Tomato', - 'Coconut', - 'Avocado', - 'Eggplant', - 'Potato', - 'Carrot', - 'Ear of Corn', - 'Hot Pepper', - 'Cucumber', - 'Leafy Green', - 'Broccoli', - 'Mushroom', - 'Peanuts', - 'Chestnut', - 'Bread', - 'Croissant', - 'Baguette Bread', - 'Pretzel', - 'Bagel', - 'Pancakes', - 'Cheese Wedge', - 'Meat on Bone', - 'Poultry Leg', - 'Cut of Meat', - 'Bacon', - 'Hamburger', - 'French Fries', - 'Pizza', - 'Hot Dog', - 'Sandwich', - 'Taco', - 'Burrito', - 'Stuffed Flatbread', - 'Cooking', - 'Shallow Pan of Food', - 'Pot of Food', - 'Bowl With Spoon', - 'Green Salad', - 'Popcorn', - 'Salt', - 'Canned Food', - 'Bento Box', - 'Rice Cracker', - 'Rice Ball', - 'Cooked Rice', - 'Curry Rice', - 'Steaming Bowl', - 'Spaghetti', - 'Roasted Sweet Potato', - 'Oden', - 'Sushi', - 'Fried Shrimp', - 'Fish Cake With Swirl', - 'Moon Cake', - 'Dango', - 'Dumpling', - 'Fortune Cookie', - 'Takeout Box', - 'Soft Ice Cream', - 'Shaved Ice', - 'Ice Cream', - 'Doughnut', - 'Cookie', - 'Birthday Cake', - 'Shortcake', - 'Cupcake', - 'Pie', - 'Chocolate Bar', - 'Candy', - 'Lollipop', - 'Custard', - 'Honey Pot', - 'Baby Bottle', - 'Glass of Milk', - 'Hot Beverage', - 'Teacup Without Handle', - 'Sake', - 'Bottle With Popping Cork', - 'Wine Glass', - 'Cocktail Glass', - 'Tropical Drink', - 'Beer Mug', - 'Clinking Beer Mugs', - 'Clinking Glasses', - 'Tumbler Glass', - 'Cup With Straw', - 'Chopsticks', - 'Fork and Knife With Plate', - 'Fork and Knife', - 'Spoon', -], [ - '🍇', - '🍈', - '🍉', - '🍊', - '🍋', - '🍌', - '🍍', - '🥭', - '🍎', - '🍏', - '🍐', - '🍑', - '🍒', - '🍓', - '🥝', - '🍅', - '🥥', - '🥑', - '🍆', - '🥔', - '🥕', - '🌽', - '🌶', - '🥒', - '🥬', - '🥦', - '🍄', - '🥜', - '🌰', - '🍞', - '🥐', - '🥖', - '🥨', - '🥯', - '🥞', - '🧀', - '🍖', - '🍗', - '🥩', - '🥓', - '🍔', - '🍟', - '🍕', - '🌭', - '🥪', - '🌮', - '🌯', - '🥙', - '🍳', - '🥘', - '🍲', - '🥣', - '🥗', - '🍿', - '🧂', - '🥫', - '🍱', - '🍘', - '🍙', - '🍚', - '🍛', - '🍜', - '🍝', - '🍠', - '🍢', - '🍣', - '🍤', - '🍥', - '🥮', - '🍡', - '🥟', - '🥠', - '🥡', - '🍦', - '🍧', - '🍨', - '🍩', - '🍪', - '🎂', - '🍰', - '🧁', - '🥧', - '🍫', - '🍬', - '🍭', - '🍮', - '🍯', - '🍼', - '🥛', - '☕', - '🍵', - '🍶', - '🍾', - '🍷', - '🍸', - '🍹', - '🍺', - '🍻', - '🥂', - '🥃', - '🥤', - '🥢', - '🍽', - '🍴', - '🥄', -]); - -/// Map of all possible emojis along with their names in [Category.TRAVEL] -final Map travel = Map.fromIterables([ - 'Person Rowing Boat', - 'Map of Japan', - 'Snow-Capped Mountain', - 'Mountain', - 'Volcano', - 'Mount Fuji', - 'Camping', - 'Beach With Umbrella', - 'Desert', - 'Desert Island', - 'National Park', - 'Stadium', - 'Classical Building', - 'Building Construction', - 'Houses', - 'Derelict House', - 'House', - 'House With Garden', - 'Office Building', - 'Japanese Post Office', - 'Post Office', - 'Hospital', - 'Bank', - 'Hotel', - 'Love Hotel', - 'Convenience Store', - 'School', - 'Department Store', - 'Factory', - 'Japanese Castle', - 'Castle', - 'Wedding', - 'Tokyo Tower', - 'Statue of Liberty', - 'Church', - 'Mosque', - 'Synagogue', - 'Shinto Shrine', - 'Kaaba', - 'Fountain', - 'Tent', - 'Foggy', - 'Night With Stars', - 'Cityscape', - 'Sunrise Over Mountains', - 'Sunrise', - 'Cityscape at Dusk', - 'Sunset', - 'Bridge at Night', - 'Carousel Horse', - 'Ferris Wheel', - 'Roller Coaster', - 'Locomotive', - 'Railway Car', - 'High-Speed Train', - 'Bullet Train', - 'Train', - 'Metro', - 'Light Rail', - 'Station', - 'Tram', - 'Monorail', - 'Mountain Railway', - 'Tram Car', - 'Bus', - 'Oncoming Bus', - 'Trolleybus', - 'Minibus', - 'Ambulance', - 'Fire Engine', - 'Police Car', - 'Oncoming Police Car', - 'Taxi', - 'Oncoming Taxi', - 'Automobile', - 'Oncoming Automobile', - 'Delivery Truck', - 'Articulated Lorry', - 'Tractor', - 'Racing Car', - 'Motorcycle', - 'Motor Scooter', - 'Bicycle', - 'Kick Scooter', - 'Bus Stop', - 'Railway Track', - 'Fuel Pump', - 'Police Car Light', - 'Horizontal Traffic Light', - 'Vertical Traffic Light', - 'Construction', - 'Anchor', - 'Sailboat', - 'Speedboat', - 'Passenger Ship', - 'Ferry', - 'Motor Boat', - 'Ship', - 'Airplane', - 'Small Airplane', - 'Airplane Departure', - 'Airplane Arrival', - 'Seat', - 'Helicopter', - 'Suspension Railway', - 'Mountain Cableway', - 'Aerial Tramway', - 'Satellite', - 'Rocket', - 'Flying Saucer', - 'Shooting Star', - 'Milky Way', - 'Umbrella on Ground', - 'Fireworks', - 'Sparkler', - 'Moon Viewing Ceremony', - 'Yen Banknote', - 'Dollar Banknote', - 'Euro Banknote', - 'Pound Banknote', - 'Moai', - 'Passport Control', - 'Customs', - 'Baggage Claim', - 'Left Luggage', -], [ - '🚣', - '🗾', - '🏔', - '⛰', - '🌋', - '🗻', - '🏕', - '🏖', - '🏜', - '🏝', - '🏞', - '🏟', - '🏛', - '🏗', - '🏘', - '🏚', - '🏠', - '🏡', - '🏢', - '🏣', - '🏤', - '🏥', - '🏦', - '🏨', - '🏩', - '🏪', - '🏫', - '🏬', - '🏭', - '🏯', - '🏰', - '💒', - '🗼', - '🗽', - '⛪', - '🕌', - '🕍', - '⛩', - '🕋', - '⛲', - '⛺', - '🌁', - '🌃', - '🏙', - '🌄', - '🌅', - '🌆', - '🌇', - '🌉', - '🎠', - '🎡', - '🎢', - '🚂', - '🚃', - '🚄', - '🚅', - '🚆', - '🚇', - '🚈', - '🚉', - '🚊', - '🚝', - '🚞', - '🚋', - '🚌', - '🚍', - '🚎', - '🚐', - '🚑', - '🚒', - '🚓', - '🚔', - '🚕', - '🚖', - '🚗', - '🚘', - '🚚', - '🚛', - '🚜', - '🏎', - '🏍', - '🛵', - '🚲', - '🛴', - '🚏', - '🛤', - '⛽', - '🚨', - '🚥', - '🚦', - '🚧', - '⚓', - '⛵', - '🚤', - '🛳', - '⛴', - '🛥', - '🚢', - '✈', - '🛩', - '🛫', - '🛬', - '💺', - '🚁', - '🚟', - '🚠', - '🚡', - '🛰', - '🚀', - '🛸', - '🌠', - '🌌', - '⛱', - '🎆', - '🎇', - '🎑', - '💴', - '💵', - '💶', - '💷', - '🗿', - '🛂', - '🛃', - '🛄', - '🛅', -]); - -/// Map of all possible emojis along with their names in [Category.ACTIVITIES] -final Map activities = Map.fromIterables([ - 'Man in Suit Levitating', - 'Man Climbing', - 'Woman Climbing', - 'Horse Racing', - 'Skier', - 'Snowboarder', - 'Man Golfing', - 'Woman Golfing', - 'Man Surfing', - 'Woman Surfing', - 'Man Rowing Boat', - 'Woman Rowing Boat', - 'Man Swimming', - 'Woman Swimming', - 'Man Bouncing Ball', - 'Woman Bouncing Ball', - 'Man Lifting Weights', - 'Woman Lifting Weights', - 'Man Biking', - 'Woman Biking', - 'Man Mountain Biking', - 'Woman Mountain Biking', - 'Man Cartwheeling', - 'Woman Cartwheeling', - 'Men Wrestling', - 'Women Wrestling', - 'Man Playing Water Polo', - 'Woman Playing Water Polo', - 'Man Playing Handball', - 'Woman Playing Handball', - 'Man Juggling', - 'Woman Juggling', - 'Man in Lotus Position', - 'Woman in Lotus Position', - 'Circus Tent', - 'Skateboard', - 'Reminder Ribbon', - 'Admission Tickets', - 'Ticket', - 'Military Medal', - 'Trophy', - 'Sports Medal', - '1st Place Medal', - '2nd Place Medal', - '3rd Place Medal', - 'Soccer Ball', - 'Baseball', - 'Softball', - 'Basketball', - 'Volleyball', - 'American Football', - 'Rugby Football', - 'Tennis', - 'Flying Disc', - 'Bowling', - 'Cricket Game', - 'FieldPB Hockey', - 'Ice Hockey', - 'Lacrosse', - 'Ping Pong', - 'Badminton', - 'Boxing Glove', - 'Martial Arts Uniform', - 'Flag in Hole', - 'Ice Skate', - 'Fishing Pole', - 'Running Shirt', - 'Skis', - 'Sled', - 'Curling Stone', - 'Direct Hit', - 'Pool 8 Ball', - 'Video Game', - 'Slot Machine', - 'Game Die', - 'Jigsaw', - 'Chess Pawn', - 'Performing Arts', - 'Artist Palette', - 'Thread', - 'Yarn', - 'Musical Score', - 'Microphone', - 'Headphone', - 'Saxophone', - 'Guitar', - 'Musical Keyboard', - 'Trumpet', - 'Violin', - 'Drum', - 'Clapper Board', - 'Bow and Arrow', -], [ - '🕴', - '🧗', - '🧗', - '🏇', - '⛷', - '🏂', - '🏌️', - '🏌️', - '🏄', - '🏄', - '🚣', - '🚣', - '🏊', - '🏊', - '⛹️', - '⛹️', - '🏋️', - '🏋️', - '🚴', - '🚴', - '🚵', - '🚵', - '🤸', - '🤸', - '🤼', - '🤼', - '🤽', - '🤽', - '🤾', - '🤾', - '🤹', - '🤹', - '🧘🏻‍♂️', - '🧘🏻‍♀️', - '🎪', - '🛹', - '🎗', - '🎟', - '🎫', - '🎖', - '🏆', - '🏅', - '🥇', - '🥈', - '🥉', - '⚽', - '⚾', - '🥎', - '🏀', - '🏐', - '🏈', - '🏉', - '🎾', - '🥏', - '🎳', - '🏏', - '🏑', - '🏒', - '🥍', - '🏓', - '🏸', - '🥊', - '🥋', - '⛳', - '⛸', - '🎣', - '🎽', - '🎿', - '🛷', - '🥌', - '🎯', - '🎱', - '🎮', - '🎰', - '🎲', - '🧩', - '♟', - '🎭', - '🎨', - '🧵', - '🧶', - '🎼', - '🎤', - '🎧', - '🎷', - '🎸', - '🎹', - '🎺', - '🎻', - '🥁', - '🎬', - '🏹', -]); - -/// Map of all possible emojis along with their names in [Category.OBJECTS] -final Map objects = Map.fromIterables([ - 'Love Letter', - 'Hole', - 'Bomb', - 'Person Taking Bath', - 'Person in Bed', - 'Kitchen Knife', - 'Amphora', - 'World Map', - 'Compass', - 'Brick', - 'Barber Pole', - 'Oil Drum', - 'Bellhop Bell', - 'Luggage', - 'Hourglass Done', - 'Hourglass Not Done', - 'Watch', - 'Alarm Clock', - 'Stopwatch', - 'Timer Clock', - 'Mantelpiece Clock', - 'Thermometer', - 'Umbrella on Ground', - 'Firecracker', - 'Balloon', - 'Party Popper', - 'Confetti Ball', - 'Japanese Dolls', - 'Carp Streamer', - 'Wind Chime', - 'Red Envelope', - 'Ribbon', - 'Wrapped Gift', - 'Crystal Ball', - 'Nazar Amulet', - 'Joystick', - 'Teddy Bear', - 'Framed Picture', - 'Thread', - 'Yarn', - 'Shopping Bags', - 'Prayer Beads', - 'Gem Stone', - 'Postal Horn', - 'Studio Microphone', - 'Level Slider', - 'Control Knobs', - 'Radio', - 'Mobile Phone', - 'Mobile Phone With Arrow', - 'Telephone', - 'Telephone Receiver', - 'Pager', - 'Fax Machine', - 'Battery', - 'Electric Plug', - 'Laptop Computer', - 'Desktop Computer', - 'Printer', - 'Keyboard', - 'Computer Mouse', - 'Trackball', - 'Computer Disk', - 'Floppy Disk', - 'Optical Disk', - 'DVD', - 'Abacus', - 'Movie Camera', - 'Film Frames', - 'Film Projector', - 'Television', - 'Camera', - 'Camera With Flash', - 'Video Camera', - 'Videocassette', - 'Magnifying Glass Tilted Left', - 'Magnifying Glass Tilted Right', - 'Candle', - 'Light Bulb', - 'Flashlight', - 'Red Paper Lantern', - 'Notebook With Decorative Cover', - 'Closed Book', - 'Open Book', - 'Green Book', - 'Blue Book', - 'Orange Book', - 'Books', - 'Notebook', - 'Page With Curl', - 'Scroll', - 'Page Facing Up', - 'Newspaper', - 'Rolled-Up Newspaper', - 'Bookmark Tabs', - 'Bookmark', - 'Label', - 'Money Bag', - 'Yen Banknote', - 'Dollar Banknote', - 'Euro Banknote', - 'Pound Banknote', - 'Money With Wings', - 'Credit Card', - 'Receipt', - 'Envelope', - 'E-Mail', - 'Incoming Envelope', - 'Envelope With Arrow', - 'Outbox Tray', - 'Inbox Tray', - 'Package', - 'Closed Mailbox With Raised Flag', - 'Closed Mailbox With Lowered Flag', - 'Open Mailbox With Raised Flag', - 'Open Mailbox With Lowered Flag', - 'Postbox', - 'Ballot Box With Ballot', - 'Pencil', - 'Black Nib', - 'Fountain Pen', - 'Pen', - 'Paintbrush', - 'Crayon', - 'Memo', - 'File Folder', - 'Open File Folder', - 'Card Index Dividers', - 'Calendar', - 'Tear-Off Calendar', - 'Spiral Notepad', - 'Spiral Calendar', - 'Card Index', - 'Chart Increasing', - 'Chart Decreasing', - 'Bar Chart', - 'Clipboard', - 'Pushpin', - 'Round Pushpin', - 'Paperclip', - 'Linked Paperclips', - 'Straight Ruler', - 'Triangular Ruler', - 'Scissors', - 'Card File Box', - 'File Cabinet', - 'Wastebasket', - 'Locked', - 'Unlocked', - 'Locked With Pen', - 'Locked With Key', - 'Key', - 'Old Key', - 'Hammer', - 'Pick', - 'Hammer and Pick', - 'Hammer and Wrench', - 'Dagger', - 'Crossed Swords', - 'Pistol', - 'Shield', - 'Wrench', - 'Nut and Bolt', - 'Gear', - 'Clamp', - 'Balance Scale', - 'Link', - 'Chains', - 'Toolbox', - 'Magnet', - 'Alembic', - 'Test Tube', - 'Petri Dish', - 'DNA', - 'Microscope', - 'Telescope', - 'Satellite Antenna', - 'Syringe', - 'Pill', - 'Door', - 'Bed', - 'Couch and Lamp', - 'Toilet', - 'Shower', - 'Bathtub', - 'Lotion Bottle', - 'Safety Pin', - 'Broom', - 'Basket', - 'Roll of Paper', - 'Soap', - 'Sponge', - 'Fire Extinguisher', - 'Cigarette', - 'Coffin', - 'Funeral Urn', - 'Moai', - 'Potable Water', -], [ - '💌', - '🕳', - '💣', - '🛀', - '🛌', - '🔪', - '🏺', - '🗺', - '🧭', - '🧱', - '💈', - '🛢', - '🛎', - '🧳', - '⌛', - '⏳', - '⌚', - '⏰', - '⏱', - '⏲', - '🕰', - '🌡', - '⛱', - '🧨', - '🎈', - '🎉', - '🎊', - '🎎', - '🎏', - '🎐', - '🧧', - '🎀', - '🎁', - '🔮', - '🧿', - '🕹', - '🧸', - '🖼', - '🧵', - '🧶', - '🛍', - '📿', - '💎', - '📯', - '🎙', - '🎚', - '🎛', - '📻', - '📱', - '📲', - '☎', - '📞', - '📟', - '📠', - '🔋', - '🔌', - '💻', - '🖥', - '🖨', - '⌨', - '🖱', - '🖲', - '💽', - '💾', - '💿', - '📀', - '🧮', - '🎥', - '🎞', - '📽', - '📺', - '📷', - '📸', - '📹', - '📼', - '🔍', - '🔎', - '🕯', - '💡', - '🔦', - '🏮', - '📔', - '📕', - '📖', - '📗', - '📘', - '📙', - '📚', - '📓', - '📃', - '📜', - '📄', - '📰', - '🗞', - '📑', - '🔖', - '🏷', - '💰', - '💴', - '💵', - '💶', - '💷', - '💸', - '💳', - '🧾', - '✉', - '📧', - '📨', - '📩', - '📤', - '📥', - '📦', - '📫', - '📪', - '📬', - '📭', - '📮', - '🗳', - '✏', - '✒', - '🖋', - '🖊', - '🖌', - '🖍', - '📝', - '📁', - '📂', - '🗂', - '📅', - '📆', - '🗒', - '🗓', - '📇', - '📈', - '📉', - '📊', - '📋', - '📌', - '📍', - '📎', - '🖇', - '📏', - '📐', - '✂', - '🗃', - '🗄', - '🗑', - '🔒', - '🔓', - '🔏', - '🔐', - '🔑', - '🗝', - '🔨', - '⛏', - '⚒', - '🛠', - '🗡', - '⚔', - '🔫', - '🛡', - '🔧', - '🔩', - '⚙', - '🗜', - '⚖', - '🔗', - '⛓', - '🧰', - '🧲', - '⚗', - '🧪', - '🧫', - '🧬', - '🔬', - '🔭', - '📡', - '💉', - '💊', - '🚪', - '🛏', - '🛋', - '🚽', - '🚿', - '🛁', - '🧴', - '🧷', - '🧹', - '🧺', - '🧻', - '🧼', - '🧽', - '🧯', - '🚬', - '⚰', - '⚱', - '🗿', - '🚰', -]); - -/// Map of all possible emojis along with their names in [Category.SYMBOLS] -final Map symbols = Map.fromIterables([ - 'Heart With Arrow', - 'Heart With Ribbon', - 'Sparkling Heart', - 'Growing Heart', - 'Beating Heart', - 'Revolving Hearts', - 'Two Hearts', - 'Heart Decoration', - 'Heavy Heart Exclamation', - 'Broken Heart', - 'Red Heart', - 'Orange Heart', - 'Yellow Heart', - 'Green Heart', - 'Blue Heart', - 'Purple Heart', - 'Black Heart', - 'Hundred Points', - 'Anger Symbol', - 'Speech Balloon', - 'Eye in Speech Bubble', - 'Right Anger Bubble', - 'Thought Balloon', - 'Zzz', - 'White Flower', - 'Hot Springs', - 'Barber Pole', - 'Stop Sign', - 'Twelve O’Clock', - 'Twelve-Thirty', - 'One O’Clock', - 'One-Thirty', - 'Two O’Clock', - 'Two-Thirty', - 'Three O’Clock', - 'Three-Thirty', - 'Four O’Clock', - 'Four-Thirty', - 'Five O’Clock', - 'Five-Thirty', - 'Six O’Clock', - 'Six-Thirty', - 'Seven O’Clock', - 'Seven-Thirty', - 'Eight O’Clock', - 'Eight-Thirty', - 'Nine O’Clock', - 'Nine-Thirty', - 'Ten O’Clock', - 'Ten-Thirty', - 'Eleven O’Clock', - 'Eleven-Thirty', - 'Cyclone', - 'Spade Suit', - 'Heart Suit', - 'Diamond Suit', - 'Club Suit', - 'Joker', - 'Mahjong Red Dragon', - 'Flower Playing Cards', - 'Muted Speaker', - 'Speaker Low Volume', - 'Speaker Medium Volume', - 'Speaker High Volume', - 'Loudspeaker', - 'Megaphone', - 'Postal Horn', - 'Bell', - 'Bell With Slash', - 'Musical Note', - 'Musical Notes', - 'ATM Sign', - 'Litter in Bin Sign', - 'Potable Water', - 'Wheelchair Symbol', - 'Men’s Room', - 'Women’s Room', - 'Restroom', - 'Baby Symbol', - 'Water Closet', - 'Warning', - 'Children Crossing', - 'No Entry', - 'Prohibited', - 'No Bicycles', - 'No Smoking', - 'No Littering', - 'Non-Potable Water', - 'No Pedestrians', - 'No One Under Eighteen', - 'Radioactive', - 'Biohazard', - 'Up Arrow', - 'Up-Right Arrow', - 'Right Arrow', - 'Down-Right Arrow', - 'Down Arrow', - 'Down-Left Arrow', - 'Left Arrow', - 'Up-Left Arrow', - 'Up-Down Arrow', - 'Left-Right Arrow', - 'Right Arrow Curving Left', - 'Left Arrow Curving Right', - 'Right Arrow Curving Up', - 'Right Arrow Curving Down', - 'Clockwise Vertical Arrows', - 'Counterclockwise Arrows Button', - 'Back Arrow', - 'End Arrow', - 'On! Arrow', - 'Soon Arrow', - 'Top Arrow', - 'Place of Worship', - 'Atom Symbol', - 'Om', - 'Star of David', - 'Wheel of Dharma', - 'Yin Yang', - 'Latin Cross', - 'Orthodox Cross', - 'Star and Crescent', - 'Peace Symbol', - 'Menorah', - 'Dotted Six-Pointed Star', - 'Aries', - 'Taurus', - 'Gemini', - 'Cancer', - 'Leo', - 'Virgo', - 'Libra', - 'Scorpio', - 'Sagittarius', - 'Capricorn', - 'Aquarius', - 'Pisces', - 'Ophiuchus', - 'Shuffle Tracks Button', - 'Repeat Button', - 'Repeat Single Button', - 'Play Button', - 'Fast-Forward Button', - 'Reverse Button', - 'Fast Reverse Button', - 'Upwards Button', - 'Fast Up Button', - 'Downwards Button', - 'Fast Down Button', - 'Stop Button', - 'Eject Button', - 'Cinema', - 'Dim Button', - 'Bright Button', - 'Antenna Bars', - 'Vibration Mode', - 'Mobile Phone Off', - 'Infinity', - 'Recycling Symbol', - 'Trident Emblem', - 'Name Badge', - 'Japanese Symbol for Beginner', - 'Heavy Large Circle', - 'White Heavy Check Mark', - 'Ballot Box With Check', - 'Heavy Check Mark', - 'Heavy Multiplication X', - 'Cross Mark', - 'Cross Mark Button', - 'Heavy Plus Sign', - 'Heavy Minus Sign', - 'Heavy Division Sign', - 'Curly Loop', - 'Double Curly Loop', - 'Part Alternation Mark', - 'Eight-Spoked Asterisk', - 'Eight-Pointed Star', - 'Sparkle', - 'Double Exclamation Mark', - 'Exclamation Question Mark', - 'Question Mark', - 'White Question Mark', - 'White Exclamation Mark', - 'Exclamation Mark', - 'Copyright', - 'Registered', - 'Trade Mark', - 'Keycap Number Sign', - 'Keycap Digit Zero', - 'Keycap Digit One', - 'Keycap Digit Two', - 'Keycap Digit Three', - 'Keycap Digit Four', - 'Keycap Digit Five', - 'Keycap Digit Six', - 'Keycap Digit Seven', - 'Keycap Digit Eight', - 'Keycap Digit Nine', - 'Keycap: 10', - 'Input Latin Uppercase', - 'Input Latin Lowercase', - 'Input Numbers', - 'Input Symbols', - 'Input Latin Letters', - 'A Button (Blood Type)', - 'AB Button (Blood Type)', - 'B Button (Blood Type)', - 'CL Button', - 'Cool Button', - 'Free Button', - 'Information', - 'ID Button', - 'Circled M', - 'New Button', - 'NG Button', - 'O Button (Blood Type)', - 'OK Button', - 'P Button', - 'SOS Button', - 'Up! Button', - 'Vs Button', - 'Japanese “Here” Button', - 'Japanese “Service Charge” Button', - 'Japanese “Monthly Amount” Button', - 'Japanese “Not Free of Charge” Button', - 'Japanese “Reserved” Button', - 'Japanese “Bargain” Button', - 'Japanese “Discount” Button', - 'Japanese “Free of Charge” Button', - 'Japanese “Prohibited” Button', - 'Japanese “Acceptable” Button', - 'Japanese “Application” Button', - 'Japanese “Passing Grade” Button', - 'Japanese “Vacancy” Button', - 'Japanese “Congratulations” Button', - 'Japanese “Secret” Button', - 'Japanese “Open for Business” Button', - 'Japanese “No Vacancy” Button', - 'Red Circle', - 'Blue Circle', - 'Black Circle', - 'White Circle', - 'Black Large Square', - 'White Large Square', - 'Black Medium Square', - 'White Medium Square', - 'Black Medium-Small Square', - 'White Medium-Small Square', - 'Black Small Square', - 'White Small Square', - 'Large Orange Diamond', - 'Large Blue Diamond', - 'Small Orange Diamond', - 'Small Blue Diamond', - 'Red Triangle Pointed Up', - 'Red Triangle Pointed Down', - 'Diamond With a Dot', - 'White Square Button', - 'Black Square Button', -], [ - '💘', - '💝', - '💖', - '💗', - '💓', - '💞', - '💕', - '💟', - '❣', - '💔', - '❤', - '🧡', - '💛', - '💚', - '💙', - '💜', - '🖤', - '💯', - '💢', - '💬', - '👁️‍🗨️', - '🗯', - '💭', - '💤', - '💮', - '♨', - '💈', - '🛑', - '🕛', - '🕧', - '🕐', - '🕜', - '🕑', - '🕝', - '🕒', - '🕞', - '🕓', - '🕟', - '🕔', - '🕠', - '🕕', - '🕡', - '🕖', - '🕢', - '🕗', - '🕣', - '🕘', - '🕤', - '🕙', - '🕥', - '🕚', - '🕦', - '🌀', - '♠', - '♥', - '♦', - '♣', - '🃏', - '🀄', - '🎴', - '🔇', - '🔈', - '🔉', - '🔊', - '📢', - '📣', - '📯', - '🔔', - '🔕', - '🎵', - '🎶', - '🏧', - '🚮', - '🚰', - '♿', - '🚹', - '🚺', - '🚻', - '🚼', - '🚾', - '⚠', - '🚸', - '⛔', - '🚫', - '🚳', - '🚭', - '🚯', - '🚱', - '🚷', - '🔞', - '☢', - '☣', - '⬆', - '↗', - '➡', - '↘', - '⬇', - '↙', - '⬅', - '↖', - '↕', - '↔', - '↩', - '↪', - '⤴', - '⤵', - '🔃', - '🔄', - '🔙', - '🔚', - '🔛', - '🔜', - '🔝', - '🛐', - '⚛', - '🕉', - '✡', - '☸', - '☯', - '✝', - '☦', - '☪', - '☮', - '🕎', - '🔯', - '♈', - '♉', - '♊', - '♋', - '♌', - '♍', - '♎', - '♏', - '♐', - '♑', - '♒', - '♓', - '⛎', - '🔀', - '🔁', - '🔂', - '▶', - '⏩', - '◀', - '⏪', - '🔼', - '⏫', - '🔽', - '⏬', - '⏹', - '⏏', - '🎦', - '🔅', - '🔆', - '📶', - '📳', - '📴', - '♾', - '♻', - '🔱', - '📛', - '🔰', - '⭕', - '✅', - '☑', - '✔', - '✖', - '❌', - '❎', - '➕', - '➖', - '➗', - '➰', - '➿', - '〽', - '✳', - '✴', - '❇', - '‼', - '⁉', - '❓', - '❔', - '❕', - '❗', - '©', - '®', - '™', - '#️⃣', - '0️⃣', - '1️⃣', - '2️⃣', - '3️⃣', - '4️⃣', - '5️⃣', - '6️⃣', - '7️⃣', - '8️⃣', - '9️⃣', - '🔟', - '🔠', - '🔡', - '🔢', - '🔣', - '🔤', - '🅰', - '🆎', - '🅱', - '🆑', - '🆒', - '🆓', - 'ℹ', - '🆔', - 'Ⓜ', - '🆕', - '🆖', - '🅾', - '🆗', - '🅿', - '🆘', - '🆙', - '🆚', - '🈁', - '🈂', - '🈷', - '🈶', - '🈯', - '🉐', - '🈹', - '🈚', - '🈲', - '🉑', - '🈸', - '🈴', - '🈳', - '㊗', - '㊙', - '🈺', - '🈵', - '🔴', - '🔵', - '⚫', - '⚪', - '⬛', - '⬜', - '◼', - '◻', - '◾', - '◽', - '▪', - '▫', - '🔶', - '🔷', - '🔸', - '🔹', - '🔺', - '🔻', - '💠', - '🔳', - '🔲', -]); - -/// Map of all possible emojis along with their names in [Category.FLAGS] -final Map flags = Map.fromIterables([ - 'Chequered Flag', - 'Triangular Flag', - 'Crossed Flags', - 'Black Flag', - 'White Flag', - 'Rainbow Flag', - 'Pirate Flag', - 'Flag: Ascension Island', - 'Flag: Andorra', - 'Flag: United Arab Emirates', - 'Flag: Afghanistan', - 'Flag: Antigua & Barbuda', - 'Flag: Anguilla', - 'Flag: Albania', - 'Flag: Armenia', - 'Flag: Angola', - 'Flag: Antarctica', - 'Flag: argentina', - 'Flag: American Samoa', - 'Flag: Austria', - 'Flag: Australia', - 'Flag: Aruba', - 'Flag: Åland Islands', - 'Flag: Azerbaijan', - 'Flag: Bosnia & Herzegovina', - 'Flag: Barbados', - 'Flag: Bangladesh', - 'Flag: Belgium', - 'Flag: Burkina Faso', - 'Flag: Bulgaria', - 'Flag: Bahrain', - 'Flag: Burundi', - 'Flag: Benin', - 'Flag: St. Barthélemy', - 'Flag: Bermuda', - 'Flag: Brunei', - 'Flag: Bolivia', - 'Flag: Caribbean Netherlands', - 'Flag: Brazil', - 'Flag: Bahamas', - 'Flag: Bhutan', - 'Flag: Bouvet Island', - 'Flag: Botswana', - 'Flag: Belarus', - 'Flag: Belize', - 'Flag: Canada', - 'Flag: Cocos (Keeling) Islands', - 'Flag: Congo - Kinshasa', - 'Flag: Central African Republic', - 'Flag: Congo - Brazzaville', - 'Flag: Switzerland', - 'Flag: Côte d’Ivoire', - 'Flag: Cook Islands', - 'Flag: Chile', - 'Flag: Cameroon', - 'Flag: China', - 'Flag: Colombia', - 'Flag: Clipperton Island', - 'Flag: Costa Rica', - 'Flag: Cuba', - 'Flag: Cape Verde', - 'Flag: Curaçao', - 'Flag: Christmas Island', - 'Flag: Cyprus', - 'Flag: Czechia', - 'Flag: Germany', - 'Flag: Diego Garcia', - 'Flag: Djibouti', - 'Flag: Denmark', - 'Flag: Dominica', - 'Flag: Dominican Republic', - 'Flag: Algeria', - 'Flag: Ceuta & Melilla', - 'Flag: Ecuador', - 'Flag: Estonia', - 'Flag: Egypt', - 'Flag: Western Sahara', - 'Flag: Eritrea', - 'Flag: Spain', - 'Flag: Ethiopia', - 'Flag: European Union', - 'Flag: Finland', - 'Flag: Fiji', - 'Flag: Falkland Islands', - 'Flag: Micronesia', - 'Flag: Faroe Islands', - 'Flag: france', - 'Flag: Gabon', - 'Flag: United Kingdom', - 'Flag: Grenada', - 'Flag: Georgia', - 'Flag: French Guiana', - 'Flag: Guernsey', - 'Flag: Ghana', - 'Flag: Gibraltar', - 'Flag: Greenland', - 'Flag: Gambia', - 'Flag: Guinea', - 'Flag: Guadeloupe', - 'Flag: Equatorial Guinea', - 'Flag: Greece', - 'Flag: South Georgia & South Sandwich Islands', - 'Flag: Guatemala', - 'Flag: Guam', - 'Flag: Guinea-Bissau', - 'Flag: Guyana', - 'Flag: Hong Kong SAR China', - 'Flag: Heard & McDonald Islands', - 'Flag: Honduras', - 'Flag: Croatia', - 'Flag: Haiti', - 'Flag: Hungary', - 'Flag: Canary Islands', - 'Flag: Indonesia', - 'Flag: Ireland', - 'Flag: Israel', - 'Flag: Isle of Man', - 'Flag: India', - 'Flag: British Indian Ocean Territory', - 'Flag: Iraq', - 'Flag: Iran', - 'Flag: Iceland', - 'Flag: Italy', - 'Flag: Jersey', - 'Flag: Jamaica', - 'Flag: Jordan', - 'Flag: Japan', - 'Flag: Kenya', - 'Flag: Kyrgyzstan', - 'Flag: Cambodia', - 'Flag: Kiribati', - 'Flag: Comoros', - 'Flag: St. Kitts & Nevis', - 'Flag: North Korea', - 'Flag: South Korea', - 'Flag: Kuwait', - 'Flag: Cayman Islands', - 'Flag: Kazakhstan', - 'Flag: Laos', - 'Flag: Lebanon', - 'Flag: St. Lucia', - 'Flag: Liechtenstein', - 'Flag: Sri Lanka', - 'Flag: Liberia', - 'Flag: Lesotho', - 'Flag: Lithuania', - 'Flag: Luxembourg', - 'Flag: Latvia', - 'Flag: Libya', - 'Flag: Morocco', - 'Flag: Monaco', - 'Flag: Moldova', - 'Flag: Montenegro', - 'Flag: St. Martin', - 'Flag: Madagascar', - 'Flag: Marshall Islands', - 'Flag: North Macedonia', - 'Flag: Mali', - 'Flag: Myanmar (Burma)', - 'Flag: Mongolia', - 'Flag: Macau Sar China', - 'Flag: Northern Mariana Islands', - 'Flag: Martinique', - 'Flag: Mauritania', - 'Flag: Montserrat', - 'Flag: Malta', - 'Flag: Mauritius', - 'Flag: Maldives', - 'Flag: Malawi', - 'Flag: Mexico', - 'Flag: Malaysia', - 'Flag: Mozambique', - 'Flag: Namibia', - 'Flag: New Caledonia', - 'Flag: Niger', - 'Flag: Norfolk Island', - 'Flag: Nigeria', - 'Flag: Nicaragua', - 'Flag: Netherlands', - 'Flag: Norway', - 'Flag: Nepal', - 'Flag: Nauru', - 'Flag: Niue', - 'Flag: New Zealand', - 'Flag: Oman', - 'Flag: Panama', - 'Flag: Peru', - 'Flag: French Polynesia', - 'Flag: Papua New Guinea', - 'Flag: Philippines', - 'Flag: Pakistan', - 'Flag: Poland', - 'Flag: St. Pierre & Miquelon', - 'Flag: Pitcairn Islands', - 'Flag: Puerto Rico', - 'Flag: Palestinian Territories', - 'Flag: Portugal', - 'Flag: Palau', - 'Flag: Paraguay', - 'Flag: Qatar', - 'Flag: Réunion', - 'Flag: Romania', - 'Flag: Serbia', - 'Flag: Russia', - 'Flag: Rwanda', - 'Flag: Saudi Arabia', - 'Flag: Solomon Islands', - 'Flag: Seychelles', - 'Flag: Sudan', - 'Flag: Sweden', - 'Flag: Singapore', - 'Flag: St. Helena', - 'Flag: Slovenia', - 'Flag: Svalbard & Jan Mayen', - 'Flag: Slovakia', - 'Flag: Sierra Leone', - 'Flag: San Marino', - 'Flag: Senegal', - 'Flag: Somalia', - 'Flag: Suriname', - 'Flag: South Sudan', - 'Flag: São Tomé & Príncipe', - 'Flag: El Salvador', - 'Flag: Sint Maarten', - 'Flag: Syria', - 'Flag: Swaziland', - 'Flag: Tristan Da Cunha', - 'Flag: Turks & Caicos Islands', - 'Flag: Chad', - 'Flag: French Southern Territories', - 'Flag: Togo', - 'Flag: Thailand', - 'Flag: Tajikistan', - 'Flag: Tokelau', - 'Flag: Timor-Leste', - 'Flag: Turkmenistan', - 'Flag: Tunisia', - 'Flag: Tonga', - 'Flag: Turkey', - 'Flag: Trinidad & Tobago', - 'Flag: Tuvalu', - 'Flag: Taiwan', - 'Flag: Tanzania', - 'Flag: Ukraine', - 'Flag: Uganda', - 'Flag: U.S. Outlying Islands', - 'Flag: United Nations', - 'Flag: United States', - 'Flag: Uruguay', - 'Flag: Uzbekistan', - 'Flag: Vatican City', - 'Flag: St. Vincent & Grenadines', - 'Flag: Venezuela', - 'Flag: British Virgin Islands', - 'Flag: U.S. Virgin Islands', - 'Flag: Vietnam', - 'Flag: Vanuatu', - 'Flag: Wallis & Futuna', - 'Flag: Samoa', - 'Flag: Kosovo', - 'Flag: Yemen', - 'Flag: Mayotte', - 'Flag: South Africa', - 'Flag: Zambia', - 'Flag: Zimbabwe', -], [ - '🏁', - '🚩', - '🎌', - '🏴', - '🏳', - '🏳️‍🌈', - '🏴‍☠️', - '🇦🇨', - '🇦🇩', - '🇦🇪', - '🇦🇫', - '🇦🇬', - '🇦🇮', - '🇦🇱', - '🇦🇲', - '🇦🇴', - '🇦🇶', - '🇦🇷', - '🇦🇸', - '🇦🇹', - '🇦🇺', - '🇦🇼', - '🇦🇽', - '🇦🇿', - '🇧🇦', - '🇧🇧', - '🇧🇩', - '🇧🇪', - '🇧🇫', - '🇧🇬', - '🇧🇭', - '🇧🇮', - '🇧🇯', - '🇧🇱', - '🇧🇲', - '🇧🇳', - '🇧🇴', - '🇧🇶', - '🇧🇷', - '🇧🇸', - '🇧🇹', - '🇧🇻', - '🇧🇼', - '🇧🇾', - '🇧🇿', - '🇨🇦', - '🇨🇨', - '🇨🇩', - '🇨🇫', - '🇨🇬', - '🇨🇭', - '🇨🇮', - '🇨🇰', - '🇨🇱', - '🇨🇲', - '🇨🇳', - '🇨🇴', - '🇨🇵', - '🇨🇷', - '🇨🇺', - '🇨🇻', - '🇨🇼', - '🇨🇽', - '🇨🇾', - '🇨🇿', - '🇩🇪', - '🇩🇬', - '🇩🇯', - '🇩🇰', - '🇩🇲', - '🇩🇴', - '🇩🇿', - '🇪🇦', - '🇪🇨', - '🇪🇪', - '🇪🇬', - '🇪🇭', - '🇪🇷', - '🇪🇸', - '🇪🇹', - '🇪🇺', - '🇫🇮', - '🇫🇯', - '🇫🇰', - '🇫🇲', - '🇫🇴', - '🇫🇷', - '🇬🇦', - '🇬🇧', - '🇬🇩', - '🇬🇪', - '🇬🇫', - '🇬🇬', - '🇬🇭', - '🇬🇮', - '🇬🇱', - '🇬🇲', - '🇬🇳', - '🇬🇵', - '🇬🇶', - '🇬🇷', - '🇬🇸', - '🇬🇹', - '🇬🇺', - '🇬🇼', - '🇬🇾', - '🇭🇰', - '🇭🇲', - '🇭🇳', - '🇭🇷', - '🇭🇹', - '🇭🇺', - '🇮🇨', - '🇮🇩', - '🇮🇪', - '🇮🇱', - '🇮🇲', - '🇮🇳', - '🇮🇴', - '🇮🇶', - '🇮🇷', - '🇮🇸', - '🇮🇹', - '🇯🇪', - '🇯🇲', - '🇯🇴', - '🇯🇵', - '🇰🇪', - '🇰🇬', - '🇰🇭', - '🇰🇮', - '🇰🇲', - '🇰🇳', - '🇰🇵', - '🇰🇷', - '🇰🇼', - '🇰🇾', - '🇰🇿', - '🇱🇦', - '🇱🇧', - '🇱🇨', - '🇱🇮', - '🇱🇰', - '🇱🇷', - '🇱🇸', - '🇱🇹', - '🇱🇺', - '🇱🇻', - '🇱🇾', - '🇲🇦', - '🇲🇨', - '🇲🇩', - '🇲🇪', - '🇲🇫', - '🇲🇬', - '🇲🇭', - '🇲🇰', - '🇲🇱', - '🇲🇲', - '🇲🇳', - '🇲🇴', - '🇲🇵', - '🇲🇶', - '🇲🇷', - '🇲🇸', - '🇲🇹', - '🇲🇺', - '🇲🇻', - '🇲🇼', - '🇲🇽', - '🇲🇾', - '🇲🇿', - '🇳🇦', - '🇳🇨', - '🇳🇪', - '🇳🇫', - '🇳🇬', - '🇳🇮', - '🇳🇱', - '🇳🇴', - '🇳🇵', - '🇳🇷', - '🇳🇺', - '🇳🇿', - '🇴🇲', - '🇵🇦', - '🇵🇪', - '🇵🇫', - '🇵🇬', - '🇵🇭', - '🇵🇰', - '🇵🇱', - '🇵🇲', - '🇵🇳', - '🇵🇷', - '🇵🇸', - '🇵🇹', - '🇵🇼', - '🇵🇾', - '🇶🇦', - '🇷🇪', - '🇷🇴', - '🇷🇸', - '🇷🇺', - '🇷🇼', - '🇸🇦', - '🇸🇧', - '🇸🇨', - '🇸🇩', - '🇸🇪', - '🇸🇬', - '🇸🇭', - '🇸🇮', - '🇸🇯', - '🇸🇰', - '🇸🇱', - '🇸🇲', - '🇸🇳', - '🇸🇴', - '🇸🇷', - '🇸🇸', - '🇸🇹', - '🇸🇻', - '🇸🇽', - '🇸🇾', - '🇸🇿', - '🇹🇦', - '🇹🇨', - '🇹🇩', - '🇹🇫', - '🇹🇬', - '🇹🇭', - '🇹🇯', - '🇹🇰', - '🇹🇱', - '🇹🇲', - '🇹🇳', - '🇹🇴', - '🇹🇷', - '🇹🇹', - '🇹🇻', - '🇹🇼', - '🇹🇿', - '🇺🇦', - '🇺🇬', - '🇺🇲', - '🇺🇳', - '🇺🇸', - '🇺🇾', - '🇺🇿', - '🇻🇦', - '🇻🇨', - '🇻🇪', - '🇻🇬', - '🇻🇮', - '🇻🇳', - '🇻🇺', - '🇼🇫', - '🇼🇸', - '🇽🇰', - '🇾🇪', - '🇾🇹', - '🇿🇦', - '🇿🇲', - '🇿🇼', -]); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart deleted file mode 100644 index 8621a0d55b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart +++ /dev/null @@ -1,340 +0,0 @@ -// ignore_for_file: constant_identifier_names - -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'models/emoji_category_models.dart'; -import 'emji_picker_config.dart'; -import 'default_emoji_picker_view.dart'; -import 'models/emoji_model.dart'; -import 'emoji_lists.dart' as emoji_list; -import 'emoji_view_state.dart'; -import 'models/recent_emoji_model.dart'; - -/// The emoji category shown on the category tab -enum EmojiCategory { - /// Searched emojis - SEARCH, - - /// Recent emojis - RECENT, - - /// Smiley emojis - SMILEYS, - - /// Animal emojis - ANIMALS, - - /// Food emojis - FOODS, - - /// Activity emojis - ACTIVITIES, - - /// Travel emojis - TRAVEL, - - /// Objects emojis - OBJECTS, - - /// Sumbol emojis - SYMBOLS, - - /// Flag emojis - FLAGS, -} - -/// Enum to alter the keyboard button style -enum ButtonMode { - /// Android button style - gives the button a splash color with ripple effect - MATERIAL, - - /// iOS button style - gives the button a fade out effect when pressed - CUPERTINO -} - -/// Callback function for when emoji is selected -/// -/// The function returns the selected [Emoji] as well -/// as the [EmojiCategory] from which it originated -typedef OnEmojiSelected = void Function(EmojiCategory category, Emoji emoji); - -/// Callback function for backspace button -typedef OnBackspacePressed = void Function(); - -/// Callback function for custom view -typedef EmojiViewBuilder = Widget Function( - EmojiPickerConfig config, - EmojiViewState state, -); - -/// The Emoji Keyboard widget -/// -/// This widget displays a grid of [Emoji] sorted by [EmojiCategory] -/// which the user can horizontally scroll through. -/// -/// There is also a bottombar which displays all the possible [EmojiCategory] -/// and allow the user to quickly switch to that [EmojiCategory] -class EmojiPicker extends StatefulWidget { - /// EmojiPicker for flutter - const EmojiPicker({ - super.key, - required this.onEmojiSelected, - this.onBackspacePressed, - this.config = const EmojiPickerConfig(), - this.customWidget, - }); - - /// Custom widget - final EmojiViewBuilder? customWidget; - - /// The function called when the emoji is selected - final OnEmojiSelected onEmojiSelected; - - /// The function called when backspace button is pressed - final OnBackspacePressed? onBackspacePressed; - - /// Config for customizations - final EmojiPickerConfig config; - - @override - EmojiPickerState createState() => EmojiPickerState(); -} - -class EmojiPickerState extends State { - static const platform = MethodChannel('emoji_picker_flutter'); - - List emojiCategoryGroupList = List.empty(growable: true); - List recentEmojiList = List.empty(growable: true); - late Future updateEmojiFuture; - - // Prevent emojis to be reloaded with every build - bool loaded = false; - - @override - void initState() { - super.initState(); - updateEmojiFuture = _updateEmojis(); - } - - @override - void didUpdateWidget(covariant EmojiPicker oldWidget) { - if (oldWidget.config != widget.config) { - // EmojiPickerConfig changed - rebuild EmojiPickerView completely - loaded = false; - updateEmojiFuture = _updateEmojis(); - } - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - if (!loaded) { - // Load emojis - updateEmojiFuture.then( - (value) => WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - setState(() { - loaded = true; - }); - }), - ); - - // Show loading indicator - return const Center(child: CircularProgressIndicator()); - } - if (widget.config.showRecentsTab) { - emojiCategoryGroupList[0].emoji = - recentEmojiList.map((e) => e.emoji).toList().cast(); - } - - final state = EmojiViewState( - emojiCategoryGroupList, - _getOnEmojiListener(), - widget.onBackspacePressed, - ); - - // Build - return widget.customWidget == null - ? DefaultEmojiPickerView(widget.config, state) - : widget.customWidget!(widget.config, state); - } - - // Add recent emoji handling to tap listener - OnEmojiSelected _getOnEmojiListener() { - return (category, emoji) { - if (widget.config.showRecentsTab) { - _addEmojiToRecentlyUsed(emoji).then((value) { - if (category != EmojiCategory.RECENT && mounted) { - setState(() { - // rebuild to update recent emoji tab - // when it is not current tab - }); - } - }); - } - widget.onEmojiSelected(category, emoji); - }; - } - - // Initialize emoji data - Future _updateEmojis() async { - emojiCategoryGroupList.clear(); - if (widget.config.showRecentsTab) { - recentEmojiList = await _getRecentEmojis(); - final List recentEmojiMap = - recentEmojiList.map((e) => e.emoji).toList().cast(); - emojiCategoryGroupList - .add(EmojiCategoryGroup(EmojiCategory.RECENT, recentEmojiMap)); - } - emojiCategoryGroupList.addAll([ - EmojiCategoryGroup( - EmojiCategory.SMILEYS, - await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'), - ), - EmojiCategoryGroup( - EmojiCategory.ANIMALS, - await _getAvailableEmojis(emoji_list.animals, title: 'animals'), - ), - EmojiCategoryGroup( - EmojiCategory.FOODS, - await _getAvailableEmojis(emoji_list.foods, title: 'foods'), - ), - EmojiCategoryGroup( - EmojiCategory.ACTIVITIES, - await _getAvailableEmojis( - emoji_list.activities, - title: 'activities', - ), - ), - EmojiCategoryGroup( - EmojiCategory.TRAVEL, - await _getAvailableEmojis(emoji_list.travel, title: 'travel'), - ), - EmojiCategoryGroup( - EmojiCategory.OBJECTS, - await _getAvailableEmojis(emoji_list.objects, title: 'objects'), - ), - EmojiCategoryGroup( - EmojiCategory.SYMBOLS, - await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'), - ), - EmojiCategoryGroup( - EmojiCategory.FLAGS, - await _getAvailableEmojis(emoji_list.flags, title: 'flags'), - ), - ]); - } - - // Get available emoji for given category title - Future> _getAvailableEmojis( - Map map, { - required String title, - }) async { - Map? newMap; - - // Get Emojis cached locally if available - newMap = await _restoreFilteredEmojis(title); - - if (newMap == null) { - // Check if emoji is available on this platform - newMap = await _getPlatformAvailableEmoji(map); - // Save available Emojis to local storage for faster loading next time - if (newMap != null) { - await _cacheFilteredEmojis(title, newMap); - } - } - - // Map to Emoji Object - return newMap!.entries - .map((entry) => Emoji(entry.key, entry.value)) - .toList(); - } - - // Check if emoji is available on current platform - Future?> _getPlatformAvailableEmoji( - Map emoji, - ) async { - if (Platform.isAndroid) { - Map? filtered = {}; - const delimiter = '|'; - try { - final entries = emoji.values.join(delimiter); - final keys = emoji.keys.join(delimiter); - final result = (await platform.invokeMethod( - 'checkAvailability', - {'emojiKeys': keys, 'emojiEntries': entries}, - )) as String; - final resultKeys = result.split(delimiter); - for (var i = 0; i < resultKeys.length; i++) { - filtered[resultKeys[i]] = emoji[resultKeys[i]]!; - } - } on PlatformException catch (_) { - filtered = null; - } - return filtered; - } else { - return emoji; - } - } - - // Restore locally cached emoji - Future?> _restoreFilteredEmojis(String title) async { - final prefs = await SharedPreferences.getInstance(); - final emojiJson = prefs.getString(title); - if (emojiJson == null) { - return null; - } - final emojis = - Map.from(jsonDecode(emojiJson) as Map); - return emojis; - } - - // Stores filtered emoji locally for faster access next time - Future _cacheFilteredEmojis( - String title, - Map emojis, - ) async { - final prefs = await SharedPreferences.getInstance(); - final emojiJson = jsonEncode(emojis); - await prefs.setString(title, emojiJson); - } - - // Returns list of recently used emoji from cache - Future> _getRecentEmojis() async { - final prefs = await SharedPreferences.getInstance(); - final emojiJson = prefs.getString('recent'); - if (emojiJson == null) { - return []; - } - final json = jsonDecode(emojiJson) as List; - return json.map(RecentEmoji.fromJson).toList(); - } - - // Add an emoji to recently used list or increase its counter - Future _addEmojiToRecentlyUsed(Emoji emoji) async { - final prefs = await SharedPreferences.getInstance(); - final recentEmojiIndex = recentEmojiList - .indexWhere((element) => element.emoji.emoji == emoji.emoji); - if (recentEmojiIndex != -1) { - // Already exist in recent list - // Just update counter - recentEmojiList[recentEmojiIndex].counter++; - } else { - recentEmojiList.add(RecentEmoji(emoji, 1)); - } - // Sort by counter desc - recentEmojiList.sort((a, b) => b.counter - a.counter); - // Limit entries to recentsLimit - recentEmojiList = recentEmojiList.sublist( - 0, - min(widget.config.recentsLimit, recentEmojiList.length), - ); - // save locally - await prefs.setString('recent', jsonEncode(recentEmojiList)); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker_builder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker_builder.dart deleted file mode 100644 index ea22a13662..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker_builder.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'emji_picker_config.dart'; -import 'emoji_view_state.dart'; - -/// Template class for custom implementation -/// Inherit this class to create your own EmojiPicker -abstract class EmojiPickerBuilder extends StatefulWidget { - /// Constructor - const EmojiPickerBuilder(this.config, this.state, {super.key}); - - /// Config for customizations - final EmojiPickerConfig config; - - /// State that holds current emoji data - final EmojiViewState state; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_view_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_view_state.dart deleted file mode 100644 index e6d0c5d559..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_view_state.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'models/emoji_category_models.dart'; -import 'emoji_picker.dart'; - -/// State that holds current emoji data -class EmojiViewState { - /// Constructor - EmojiViewState( - this.emojiCategoryGroupList, - this.onEmojiSelected, - this.onBackspacePressed, - ); - - /// List of all categories including their emojis - final List emojiCategoryGroupList; - - /// Callback when pressed on emoji - final OnEmojiSelected onEmojiSelected; - - /// Callback when pressed on backspace - final OnBackspacePressed? onBackspacePressed; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart deleted file mode 100644 index 4ef6e00994..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -EmojiPickerConfig buildFlowyEmojiPickerConfig(BuildContext context) { - final style = Theme.of(context); - return EmojiPickerConfig( - bgColor: style.cardColor, - categoryIconColor: style.iconTheme.color, - selectedCategoryIconColor: style.colorScheme.onSurface, - selectedCategoryIconBackgroundColor: style.colorScheme.primary, - progressIndicatorColor: style.colorScheme.primary, - backspaceColor: style.colorScheme.primary, - searchHintText: LocaleKeys.emoji_search.tr(), - serachHintTextStyle: style.textTheme.bodyMedium?.copyWith( - color: style.hintColor, - ), - serachBarEnableBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(4), - borderSide: BorderSide(color: style.dividerColor), - ), - serachBarFocusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(4), - borderSide: BorderSide( - color: style.colorScheme.primary, - ), - ), - noRecentsText: LocaleKeys.emoji_noRecent.tr(), - noRecentsStyle: style.textTheme.bodyMedium, - noEmojiFoundText: LocaleKeys.emoji_noEmojiFound.tr(), - scrollBarHandleColor: style.colorScheme.onSurface, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/emoji_category_models.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/emoji_category_models.dart deleted file mode 100644 index 65d0d652a2..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/emoji_category_models.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'emoji_model.dart'; -import '../emoji_picker.dart'; - -/// EmojiCategory with its emojis -class EmojiCategoryGroup { - EmojiCategoryGroup(this.category, this.emoji); - - final EmojiCategory category; - - /// List of emoji of this category - List emoji; - - @override - String toString() { - return 'Name: $category, Emoji: $emoji'; - } -} - -/// Class that defines the icon representing a [EmojiCategory] -class EmojiCategoryIcon { - /// Icon of Category - const EmojiCategoryIcon({ - required this.icon, - this.color = const Color(0xffd3d3d3), - this.selectedColor = const Color(0xffb2b2b2), - }); - - /// The icon to represent the category - final IconData icon; - - /// The default color of the icon - final Color color; - - /// The color of the icon once the category is selected - final Color selectedColor; -} - -/// Class used to define all the [EmojiCategoryIcon] shown for each [EmojiCategory] -/// -/// This allows the keyboard to be personalized by changing icons shown. -/// If a [EmojiCategoryIcon] is set as null or not defined during initialization, -/// the default icons will be used instead -class EmojiCategoryIcons { - /// Constructor - const EmojiCategoryIcons({ - this.recentIcon = Icons.access_time, - this.smileyIcon = Icons.tag_faces, - this.animalIcon = Icons.pets, - this.foodIcon = Icons.fastfood, - this.activityIcon = Icons.directions_run, - this.travelIcon = Icons.location_city, - this.objectIcon = Icons.lightbulb_outline, - this.symbolIcon = Icons.emoji_symbols, - this.flagIcon = Icons.flag, - this.searchIcon = Icons.search, - }); - - /// Icon for [EmojiCategory.RECENT] - final IconData recentIcon; - - /// Icon for [EmojiCategory.SMILEYS] - final IconData smileyIcon; - - /// Icon for [EmojiCategory.ANIMALS] - final IconData animalIcon; - - /// Icon for [EmojiCategory.FOODS] - final IconData foodIcon; - - /// Icon for [EmojiCategory.ACTIVITIES] - final IconData activityIcon; - - /// Icon for [EmojiCategory.TRAVEL] - final IconData travelIcon; - - /// Icon for [EmojiCategory.OBJECTS] - final IconData objectIcon; - - /// Icon for [EmojiCategory.SYMBOLS] - final IconData symbolIcon; - - /// Icon for [EmojiCategory.FLAGS] - final IconData flagIcon; - - /// Icon for [EmojiCategory.SEARCH] - final IconData searchIcon; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart deleted file mode 100644 index 90ce4d6f9b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -class FeatureFlagsPage extends StatelessWidget { - const FeatureFlagsPage({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return SettingsBody( - title: 'Feature flags', - children: [ - SeparatedColumn( - children: FeatureFlag.data.entries - .where((e) => e.key != FeatureFlag.unknown) - .map((e) => _FeatureFlagItem(featureFlag: e.key)) - .toList(), - ), - FlowyTextButton( - 'Restart the app to apply changes', - fontSize: 16.0, - fontColor: Colors.red, - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - onPressed: () async => runAppFlowy(), - ), - ], - ); - } -} - -class _FeatureFlagItem extends StatefulWidget { - const _FeatureFlagItem({required this.featureFlag}); - - final FeatureFlag featureFlag; - - @override - State<_FeatureFlagItem> createState() => _FeatureFlagItemState(); -} - -class _FeatureFlagItemState extends State<_FeatureFlagItem> { - @override - Widget build(BuildContext context) { - return ListTile( - title: FlowyText(widget.featureFlag.name, fontSize: 16.0), - subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3), - trailing: Switch.adaptive( - value: widget.featureFlag.isOn, - onChanged: (value) async { - await widget.featureFlag.update(value); - setState(() {}); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart deleted file mode 100644 index c55522b7d3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; -import 'package:flutter/material.dart'; - -class FeatureFlagScreen extends StatelessWidget { - const FeatureFlagScreen({ - super.key, - }); - - static const routeName = '/feature_flag'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Feature Flags'), - ), - body: const FeatureFlagsPage(), - ); - } -} 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 deleted file mode 100644 index c9fcb34204..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/shared/af_role_pb_extension.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:collection/collection.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; - -part 'workspace_member_bloc.freezed.dart'; - -// 1. get the workspace members -// 2. display the content based on the user role -// Owner: -// - invite member button -// - delete member button -// - member list -// Member: -// Guest: -// - member list -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 { - 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()); - } - 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, - ), - ), - ); - }, - updateSubscriptionInfo: (info) async => - emit(state.copyWith(subscriptionInfo: info)), - upgradePlan: () 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), - ); - } - }, - ); - }); - } - - final UserProfilePB userProfile; - - // if the workspace is null, use the current workspace - final UserWorkspacePB? workspace; - - late final String _workspaceId; - final UserBackendService _userBackendService; - - AFRolePB _getMyRole(List members) { - final role = members - .firstWhereOrNull( - (e) => e.email == userProfile.email, - ) - ?.role; - if (role == null) { - Log.error('Failed to get my role'); - return AFRolePB.Guest; - } - return role; - } - - Future _setCurrentWorkspaceId(String? workspaceId) async { - if (workspace != null) { - _workspaceId = workspace!.workspaceId; - } else if (workspaceId != null && workspaceId.isNotEmpty) { - _workspaceId = workspaceId; - } else { - final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); - currentWorkspace.fold((s) { - _workspaceId = s.id; - }, (e) { - assert(false, 'Failed to read current workspace: $e'); - Log.error('Failed to read current workspace: $e'); - _workspaceId = ''; - }); - } - } - - // We fetch workspace subscription info lazily as it's not needed in the first - // render of the page. - 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), - ); - } -} - -@freezed -class WorkspaceMemberEvent with _$WorkspaceMemberEvent { - const factory WorkspaceMemberEvent.initial() = Initial; - const factory WorkspaceMemberEvent.getWorkspaceMembers() = - GetWorkspaceMembers; - const factory WorkspaceMemberEvent.addWorkspaceMember(String email) = - AddWorkspaceMember; - 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 - invite, - // this event will add the member without sending an invitation - add, - remove, - updateRole, -} - -class WorkspaceMemberActionResult { - const WorkspaceMemberActionResult({ - required this.actionType, - required this.result, - }); - - final WorkspaceMemberActionType actionType; - final FlowyResult result; -} - -@freezed -class WorkspaceMemberState with _$WorkspaceMemberState { - const WorkspaceMemberState._(); - - const factory WorkspaceMemberState({ - @Default([]) List members, - @Default(AFRolePB.Guest) AFRolePB myRole, - @Default(null) WorkspaceMemberActionResult? actionResult, - @Default(true) bool isLoading, - @Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo, - }) = _WorkspaceMemberState; - - factory WorkspaceMemberState.initial() => const WorkspaceMemberState(); - - @override - int get hashCode => runtimeType.hashCode; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is WorkspaceMemberState && - other.members == members && - other.myRole == myRole && - other.subscriptionInfo == subscriptionInfo && - 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 deleted file mode 100644 index bf33ab9d72..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ /dev/null @@ -1,620 +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/shared/af_role_pb_extension.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/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: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:string_validator/string_validator.dart'; - -class WorkspaceMembersPage extends StatelessWidget { - const WorkspaceMembersPage({ - super.key, - required this.userProfile, - required this.workspaceId, - }); - - final UserProfilePB userProfile; - final String workspaceId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => WorkspaceMemberBloc(userProfile: userProfile) - ..add(const WorkspaceMemberEvent.initial()), - child: BlocConsumer( - listener: _showResultDialog, - builder: (context, state) { - return SettingsBody( - title: LocaleKeys.settings_appearance_members_title.tr(), - autoSeparate: false, - children: [ - if (state.actionResult != null) ...[ - _showMemberLimitWarning(context, state), - const VSpace(16), - ], - if (state.myRole.canInvite) ...[ - const _InviteMember(), - const SettingsCategorySpacer(), - ], - if (state.members.isNotEmpty) - _MemberList( - members: state.members, - userProfile: userProfile, - myRole: state.myRole, - ), - ], - ); - }, - ), - ); - } - - 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.invite && - 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) { - return; - } - - final actionType = actionResult.actionType; - final result = actionResult.result; - - // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.add) { - result.fold( - (s) { - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), - ); - }, - (f) { - Log.error('add workspace member failed: $f'); - final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded - ? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr() - : LocaleKeys.settings_appearance_members_failedToAddMember.tr(); - showDialog( - context: context, - builder: (context) => NavigatorOkCancelDialog(message: message), - ); - }, - ); - } else if (actionType == WorkspaceMemberActionType.invite) { - result.fold( - (s) { - 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_failedToInviteMember - .tr(); - showConfirmDialog( - context: context, - title: LocaleKeys - .settings_appearance_members_inviteFailedDialogTitle - .tr(), - description: message, - confirmLabel: LocaleKeys.button_ok.tr(), - ); - }, - ); - } - } -} - -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: PrimaryRoundedButton( - text: LocaleKeys.settings_appearance_members_sendInvite.tr(), - margin: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - onTap: _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(); - } -} - -class _MemberList extends StatelessWidget { - const _MemberList({ - required this.members, - required this.myRole, - required this.userProfile, - }); - - final List members; - final AFRolePB myRole; - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return SeparatedColumn( - crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const Divider(), - children: [ - const _MemberListHeader(), - ...members.map( - (member) => _MemberItem( - member: member, - myRole: myRole, - userProfile: userProfile, - ), - ), - ], - ); - } -} - -class _MemberListHeader extends StatelessWidget { - const _MemberListHeader(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - LocaleKeys.settings_appearance_members_label.tr(), - fontSize: 16.0, - ), - const VSpace(16.0), - Row( - children: [ - Expanded( - child: FlowyText.semibold( - LocaleKeys.settings_appearance_members_user.tr(), - fontSize: 14.0, - ), - ), - Expanded( - child: FlowyText.semibold( - LocaleKeys.settings_appearance_members_role.tr(), - fontSize: 14.0, - ), - ), - const HSpace(28.0), - ], - ), - ], - ); - } -} - -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 textColor = member.role.isOwner ? Theme.of(context).hintColor : null; - return Row( - children: [ - Expanded( - child: FlowyText.medium( - member.name, - color: textColor, - fontSize: 14.0, - ), - ), - Expanded( - child: member.role.isOwner || !myRole.canUpdate - ? FlowyText.medium( - member.role.description, - color: textColor, - fontSize: 14.0, - ) - : _MemberRoleActionList( - member: member, - ), - ), - myRole.canDelete && - member.email != userProfile.email // can't delete self - ? _MemberMoreActionList(member: member) - : const HSpace(28.0), - ], - ); - } -} - -enum _MemberMoreAction { - delete, -} - -class _MemberMoreActionList extends StatelessWidget { - const _MemberMoreActionList({ - required this.member, - }); - - final WorkspaceMemberPB member; - - @override - Widget build(BuildContext context) { - return PopoverActionList<_MemberMoreActionWrapper>( - asBarrier: true, - direction: PopoverDirection.bottomWithCenterAligned, - actions: _MemberMoreAction.values - .map((e) => _MemberMoreActionWrapper(e, member)) - .toList(), - buildChild: (controller) { - return FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.three_dots_vertical_s, - ), - onTap: () { - controller.show(); - }, - ); - }, - onSelected: (action, controller) { - switch (action.inner) { - case _MemberMoreAction.delete: - showDialog( - context: context, - builder: (_) => NavigatorOkCancelDialog( - title: LocaleKeys.settings_appearance_members_removeMember.tr(), - message: LocaleKeys - .settings_appearance_members_areYouSureToRemoveMember - .tr(), - onOkPressed: () => context.read().add( - WorkspaceMemberEvent.removeWorkspaceMember( - action.member.email, - ), - ), - okTitle: LocaleKeys.button_yes.tr(), - ), - ); - break; - } - controller.close(); - }, - ); - } -} - -class _MemberMoreActionWrapper extends ActionCell { - _MemberMoreActionWrapper(this.inner, this.member); - - final _MemberMoreAction inner; - final WorkspaceMemberPB member; - - @override - String get name { - switch (inner) { - case _MemberMoreAction.delete: - return LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(); - } - } -} - -class _MemberRoleActionList extends StatelessWidget { - const _MemberRoleActionList({ - required this.member, - }); - - final WorkspaceMemberPB member; - - @override - Widget build(BuildContext context) { - 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 deleted file mode 100644 index 5f158f4ae1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ /dev/null @@ -1,476 +0,0 @@ -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_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:flutter_bloc/flutter_bloc.dart'; - -class AppFlowyCloudViewSetting extends StatelessWidget { - const AppFlowyCloudViewSetting({ - super.key, - this.serverURL = kAppflowyCloudUrl, - this.authenticatorType = AuthenticatorType.appflowyCloud, - required this.restartAppFlowy, - }); - - final String serverURL; - final AuthenticatorType authenticatorType; - 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) => _renderContent(context, setting), - (err) => FlowyErrorPage.message(err.toString(), howToFix: ""), - ); - } - - return const Center( - child: CircularProgressIndicator(), - ); - }, - ); - } - - BlocProvider _renderContent( - BuildContext context, - CloudSettingPB setting, - ) { - return BlocProvider( - create: (context) => AppFlowyCloudSettingBloc(setting) - ..add(const AppFlowyCloudSettingEvent.initial()), - child: BlocBuilder( - 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, - ); - restartAppFlowy(); - }, - ).show(context); - }, - showRestartHint: state.showRestartHint, - ), - ], - ); - }, - ), - ); - } -} - -class CustomAppFlowyCloudView extends StatelessWidget { - const CustomAppFlowyCloudView({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) => _renderContent(setting), - (err) => FlowyErrorPage.message(err.toString(), howToFix: ""), - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - } - - BlocProvider _renderContent( - CloudSettingPB setting, - ) { - final List children = []; - children.addAll([ - const AppFlowyCloudEnableSync(), - // const AppFlowyCloudSyncLogEnabled(), - const VSpace(40), - ]); - - // If the enableCustomCloud flag is true, then the user can dynamically configure cloud settings. Otherwise, the user cannot dynamically configure cloud settings. - if (Env.enableCustomCloud) { - children.add( - AppFlowyCloudURLs(restartAppFlowy: () => restartAppFlowy()), - ); - } else { - children.add( - Row( - children: [ - FlowyText(LocaleKeys.settings_menu_cloudServerType.tr()), - const Spacer(), - const FlowyText(Env.afCloudUrl), - ], - ), - ); - } - return BlocProvider( - create: (context) => AppFlowyCloudSettingBloc(setting) - ..add(const AppFlowyCloudSettingEvent.initial()), - child: Column( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ); - } -} - -class AppFlowyCloudURLs extends StatelessWidget { - const AppFlowyCloudURLs({super.key, required this.restartAppFlowy}); - - final VoidCallback restartAppFlowy; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - AppFlowyCloudURLsBloc()..add(const AppFlowyCloudURLsEvent.initial()), - child: BlocListener( - listener: (context, state) async { - if (state.restartApp) { - restartAppFlowy(); - } - }, - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const AppFlowySelfHostTip(), - const VSpace(12), - CloudURLInput( - title: LocaleKeys.settings_menu_cloudURL.tr(), - url: state.config.base_url, - hint: LocaleKeys.settings_menu_cloudURLHint.tr(), - onChanged: (text) { - context.read().add( - AppFlowyCloudURLsEvent.updateServerUrl( - text, - ), - ); - }, - ), - 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( - title: LocaleKeys.settings_menu_restartAppTip.tr(), - confirm: () { - context.read().add( - const AppFlowyCloudURLsEvent.confirmUpdate(), - ); - }, - ).show(context); - }, - showRestartHint: state.showRestartHint, - ), - ], - ); - }, - ), - ), - ); - } -} - -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"; - - @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!, - ), - ], - ), - ), - ); - } -} - -@visibleForTesting -class CloudURLInput extends StatefulWidget { - const CloudURLInput({ - super.key, - required this.title, - 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; - - @override - CloudURLInputState createState() => CloudURLInputState(); -} - -class CloudURLInputState 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 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, - ), - ), - ], - ); - } - - Widget _buildHint(BuildContext context) { - final children = [ - FlowyText( - widget.title, - fontSize: 12, - ), - ]; - - if (widget.hintBuilder != null) { - children.add(widget.hintBuilder!(context)); - } - - return Row( - mainAxisSize: MainAxisSize.min, - children: children, - ); - } -} - -class AppFlowyCloudEnableSync extends StatelessWidget { - const AppFlowyCloudEnableSync({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Row( - children: [ - FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()), - const Spacer(), - Toggle( - value: state.setting.enableSync, - onChanged: (value) => context - .read() - .add(AppFlowyCloudSettingEvent.enableSync(value)), - ), - ], - ); - }, - ); - } -} - -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 deleted file mode 100644 index 692be99baa..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/env/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/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: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'; -import 'package:universal_platform/universal_platform.dart'; - -import 'setting_appflowy_cloud.dart'; - -class SettingCloud extends StatelessWidget { - const SettingCloud({ - super.key, - required this.restartAppFlowy, - }); - - final VoidCallback restartAppFlowy; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: getAuthenticatorType(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - final cloudType = snapshot.data!; - return BlocProvider( - create: (context) => CloudSettingBloc(cloudType), - child: BlocBuilder( - builder: (context, state) { - return SettingsBody( - title: LocaleKeys.settings_menu_cloudSettings.tr(), - autoSeparate: false, - children: [ - if (Env.enableCustomCloud) - _CloudServerSwitcher(cloudType: state.cloudType), - _viewFromCloudType(state.cloudType), - ], - ); - }, - ), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); - } - - Widget _viewFromCloudType(AuthenticatorType cloudType) { - switch (cloudType) { - case AuthenticatorType.local: - return SettingLocalCloud(restartAppFlowy: restartAppFlowy); - case AuthenticatorType.appflowyCloud: - return AppFlowyCloudViewSetting(restartAppFlowy: restartAppFlowy); - case AuthenticatorType.appflowyCloudSelfHost: - return CustomAppFlowyCloudView(restartAppFlowy: restartAppFlowy); - case AuthenticatorType.appflowyCloudDevelop: - return AppFlowyCloudViewSetting( - serverURL: "http://localhost", - authenticatorType: AuthenticatorType.appflowyCloudDevelop, - restartAppFlowy: restartAppFlowy, - ); - } - } -} - -class CloudTypeSwitcher extends StatelessWidget { - const CloudTypeSwitcher({ - super.key, - required this.cloudType, - required this.onSelected, - }); - - final AuthenticatorType cloudType; - final Function(AuthenticatorType) onSelected; - - @override - Widget build(BuildContext context) { - final isDevelopMode = integrationMode().isDevelop; - // Only show the appflowyCloudDevelop in develop mode - final values = AuthenticatorType.values.where((element) { - 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); - } - }, - options: values - .map( - (type) => buildDropdownMenuEntry( - context, - value: type, - label: titleFromCloudType(type), - ), - ) - .toList(), - ) - : FlowyButton( - text: FlowyText( - titleFromCloudType(cloudType), - ), - useIntrinsicWidth: true, - 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(), - ), - ), - ); - } -} - -class CloudTypeItem extends StatelessWidget { - const CloudTypeItem({ - super.key, - required this.cloudType, - required this.currentCloudType, - required this.onSelected, - }); - - final AuthenticatorType cloudType; - final AuthenticatorType currentCloudType; - final Function(AuthenticatorType) onSelected; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 32, - child: FlowyButton( - text: FlowyText.medium( - titleFromCloudType(cloudType), - ), - rightIcon: currentCloudType == cloudType - ? const FlowySvg(FlowySvgs.check_s) - : null, - onTap: () { - if (currentCloudType != cloudType) { - NavigatorAlertDialog( - title: LocaleKeys.settings_menu_changeServerTip.tr(), - confirm: () async { - onSelected(cloudType); - }, - hideCancelButton: true, - ).show(context); - } - PopoverContainer.of(context).close(); - }, - ), - ); - } -} - -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.appflowyCloud: - return LocaleKeys.settings_menu_cloudAppFlowy.tr(); - case AuthenticatorType.appflowyCloudSelfHost: - return LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(); - case AuthenticatorType.appflowyCloudDevelop: - return "AppFlowyCloud Develop"; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_local_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_local_cloud.dart deleted file mode 100644 index 68680c0dd0..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_local_cloud.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class SettingLocalCloud extends StatelessWidget { - const SettingLocalCloud({super.key, required this.restartAppFlowy}); - - final VoidCallback restartAppFlowy; - - @override - Widget build(BuildContext context) { - return RestartButton( - onClick: () => onPressed(context), - showRestartHint: true, - ); - } - - void onPressed(BuildContext context) { - NavigatorAlertDialog( - title: LocaleKeys.settings_menu_restartAppTip.tr(), - confirm: () async { - await useLocalServer(); - restartAppFlowy(); - }, - ).show(context); - } -} 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 deleted file mode 100644 index 8a85377efe..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ /dev/null @@ -1,72 +0,0 @@ -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/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'; -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, - }); - - final VoidCallback didLogin; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(), - child: BlocConsumer( - listener: (context, state) { - final successOrFail = state.successOrFail; - if (successOrFail != null) { - _handleSuccessOrFail(successOrFail, context); - } - }, - builder: (_, state) { - final indicator = state.isSubmitting - ? const LinearProgressIndicator(minHeight: 1) - : const SizedBox.shrink(); - - final promptMessage = state.isSubmitting - ? FlowyText.medium( - LocaleKeys.signIn_syncPromptMessage.tr(), - maxLines: null, - ) - : const SizedBox.shrink(); - - return Column( - children: [ - promptMessage, - const VSpace(6), - indicator, - const VSpace(6), - if (isAuthEnabled) const ThirdPartySignInButtons(), - ], - ); - }, - ), - ); - } - - Future _handleSuccessOrFail( - FlowyResult result, - BuildContext context, - ) async { - result.fold( - (user) async { - didLogin(); - await runAppFlowy(); - }, - (error) => showSnapBar(context, error.msg), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart new file mode 100644 index 0000000000..57c2d2a789 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart @@ -0,0 +1,160 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/appearance.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsAppearanceView extends StatelessWidget { + const SettingsAppearanceView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ThemeModeSetting(currentThemeMode: state.themeMode), + ThemeSetting(currentTheme: state.appTheme.themeName), + ], + ); + }, + ), + ); + } +} + +class ThemeSetting extends StatelessWidget { + final String currentTheme; + const ThemeSetting({ + super.key, + required this.currentTheme, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys.settings_appearance_theme.tr(), + overflow: TextOverflow.ellipsis, + ), + ), + AppFlowyPopover( + direction: PopoverDirection.bottomWithRightAligned, + child: FlowyTextButton( + currentTheme, + fontColor: Theme.of(context).colorScheme.onBackground, + fillColor: Colors.transparent, + onPressed: () {}, + ), + popupBuilder: (BuildContext context) { + return IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _themeItemButton(context, BuiltInTheme.defaultTheme), + _themeItemButton(context, BuiltInTheme.dandelion), + _themeItemButton(context, BuiltInTheme.lavender), + ], + ), + ); + }, + ), + ], + ); + } + + Widget _themeItemButton(BuildContext context, String theme) { + return SizedBox( + height: 32, + child: FlowyButton( + text: FlowyText.medium(theme), + rightIcon: currentTheme == theme + ? const FlowySvg(name: 'grid/checkmark') + : null, + onTap: () { + if (currentTheme != theme) { + context.read().setTheme(theme); + } + }, + ), + ); + } +} + +class ThemeModeSetting extends StatelessWidget { + final ThemeMode currentThemeMode; + const ThemeModeSetting({required this.currentThemeMode, super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys.settings_appearance_themeMode_label.tr(), + overflow: TextOverflow.ellipsis, + ), + ), + AppFlowyPopover( + direction: PopoverDirection.bottomWithRightAligned, + child: FlowyTextButton( + _themeModeLabelText(currentThemeMode), + fontColor: Theme.of(context).colorScheme.onBackground, + fillColor: Colors.transparent, + onPressed: () {}, + ), + popupBuilder: (BuildContext context) { + return IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _themeModeItemButton(context, ThemeMode.light), + _themeModeItemButton(context, ThemeMode.dark), + _themeModeItemButton(context, ThemeMode.system), + ], + ), + ); + }, + ), + ], + ); + } + + Widget _themeModeItemButton(BuildContext context, ThemeMode themeMode) { + return SizedBox( + height: 32, + child: FlowyButton( + text: FlowyText.medium(_themeModeLabelText(themeMode)), + rightIcon: currentThemeMode == themeMode + ? const FlowySvg(name: 'grid/checkmark') + : null, + onTap: () { + if (currentThemeMode != themeMode) { + context.read().setThemeMode(themeMode); + } + }, + ), + ); + } + + String _themeModeLabelText(ThemeMode themeMode) { + switch (themeMode) { + case (ThemeMode.light): + return LocaleKeys.settings_appearance_themeMode_light.tr(); + case (ThemeMode.dark): + return LocaleKeys.settings_appearance_themeMode_dark.tr(); + case (ThemeMode.system): + return LocaleKeys.settings_appearance_themeMode_system.tr(); + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart similarity index 87% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart index ed6c8949b7..b381305607 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_export_file_widget.dart @@ -1,15 +1,16 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart'; +import 'package:flowy_infra/image.dart'; import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:styled_widget/styled_widget.dart'; -import '../../../../../generated/locale_keys.g.dart'; +import '../../../../generated/locale_keys.g.dart'; class SettingsExportFileWidget extends StatefulWidget { - const SettingsExportFileWidget({super.key}); + const SettingsExportFileWidget({ + super.key, + }); @override State createState() => @@ -64,8 +65,8 @@ class _OpenExportedDirectoryButton extends StatelessWidget { return FlowyIconButton( hoverColor: Theme.of(context).colorScheme.secondaryContainer, tooltipText: LocaleKeys.settings_files_export.tr(), - icon: FlowySvg( - FlowySvgs.open_folder_lg, + icon: svgWidget( + 'common/open_folder', color: Theme.of(context).iconTheme.color, ), onPressed: onTap, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart new file mode 100644 index 0000000000..097d3a4631 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -0,0 +1,266 @@ +import 'dart:io'; + +import 'package:appflowy/startup/entry_point.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../../../generated/locale_keys.g.dart'; +import '../../../../startup/launch_configuration.dart'; +import '../../../../startup/startup.dart'; +import '../../../../startup/tasks/prelude.dart'; + +class SettingsFileLocationCustomizer extends StatefulWidget { + const SettingsFileLocationCustomizer({ + super.key, + }); + + @override + State createState() => + SettingsFileLocationCustomizerState(); +} + +@visibleForTesting +class SettingsFileLocationCustomizerState + extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => SettingsLocationCubit(), + child: BlocBuilder( + builder: (context, state) { + return state.when( + initial: () => const Center( + child: CircularProgressIndicator(), + ), + didReceivedPath: (path) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // display file paths. + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + LocaleKeys.settings_files_defaultLocation.tr(), + fontSize: 13, + overflow: TextOverflow.visible, + ).padding(horizontal: 5), + const VSpace(5), + _CopyableText( + usingPath: path, + ), + ], + ), + ), + + // display the icons + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: _ChangeStoragePathButton( + usingPath: path, + ), + ), + const HSpace(10), + _OpenStorageButton( + usingPath: path, + ), + _RecoverDefaultStorageButton( + usingPath: path, + ), + ], + ), + ), + ], + ); + }, + ); + }, + ), + ); + } +} + +class _CopyableText extends StatelessWidget { + const _CopyableText({ + required this.usingPath, + }); + + final String usingPath; + + @override + Widget build(BuildContext context) { + return FlowyHover( + builder: (_, onHover) { + return GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: usingPath)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: FlowyText( + LocaleKeys.settings_files_pathCopiedSnackbar.tr(), + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); + }, + child: Container( + height: 20, + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText.regular( + usingPath, + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + ), + if (onHover) ...[ + const HSpace(5), + FlowyText.regular( + LocaleKeys.settings_files_copy.tr(), + fontSize: 12, + color: Theme.of(context).colorScheme.primary, + ), + ], + ], + ), + ), + ); + }, + ); + } +} + +class _ChangeStoragePathButton extends StatefulWidget { + const _ChangeStoragePathButton({ + required this.usingPath, + }); + + final String usingPath; + + @override + State<_ChangeStoragePathButton> createState() => + _ChangeStoragePathButtonState(); +} + +class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> { + @override + Widget build(BuildContext context) { + return Tooltip( + message: LocaleKeys.settings_files_changeLocationTooltips.tr(), + child: SecondaryTextButton( + LocaleKeys.settings_files_change.tr(), + mode: SecondaryTextButtonMode.small, + onPressed: () async { + // pick the new directory and reload app + final path = await getIt().getDirectoryPath(); + if (path == null || !mounted || widget.usingPath == path) { + return; + } + await context.read().setCustomPath(path); + await FlowyRunner.run( + FlowyApp(), + integrationEnv(), + config: const LaunchConfiguration( + autoRegistrationSupported: true, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ); + } +} + +class _OpenStorageButton extends StatelessWidget { + const _OpenStorageButton({ + required this.usingPath, + }); + + final String usingPath; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + tooltipText: LocaleKeys.settings_files_openCurrentDataFolder.tr(), + icon: svgWidget( + 'common/open_folder', + color: Theme.of(context).iconTheme.color, + ), + onPressed: () async { + final uri = Directory(usingPath).uri; + if (await canLaunchUrl(uri)) { + launchUrl(uri); + } + }, + ); + } +} + +class _RecoverDefaultStorageButton extends StatefulWidget { + const _RecoverDefaultStorageButton({ + required this.usingPath, + }); + + final String usingPath; + + @override + State<_RecoverDefaultStorageButton> createState() => + _RecoverDefaultStorageButtonState(); +} + +class _RecoverDefaultStorageButtonState + extends State<_RecoverDefaultStorageButton> { + @override + Widget build(BuildContext context) { + return FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + tooltipText: LocaleKeys.settings_files_recoverLocationTooltips.tr(), + icon: svgWidget( + 'common/recover', + color: Theme.of(context).iconTheme.color, + ), + onPressed: () async { + // reset to the default directory and reload app + final directory = await appFlowyApplicationDataDirectory(); + final path = directory.path; + if (!mounted || widget.usingPath == path) { + return; + } + await context + .read() + .resetDataStoragePathToApplicationDefault(); + await FlowyRunner.run( + FlowyApp(), + integrationEnv(), + config: const LaunchConfiguration( + autoRegistrationSupported: true, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ); + } +} 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/settings_file_exporter_widget.dart similarity index 77% rename from frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart index 7c8e128ec6..f673921816 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/settings_file_exporter_widget.dart @@ -1,29 +1,25 @@ import 'dart:io'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/navigator_context_extension.dart'; +import 'package:appflowy/util/file_picker/file_picker_service.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'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:dartz/dartz.dart' as dartz; 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/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; - -import '../../../../../generated/locale_keys.g.dart'; +import '../../../../generated/locale_keys.g.dart'; class FileExporterWidget extends StatefulWidget { - const FileExporterWidget({super.key}); + const FileExporterWidget({Key? key}) : super(key: key); @override State createState() => _FileExporterWidgetState(); @@ -36,14 +32,14 @@ class _FileExporterWidgetState extends State { @override Widget build(BuildContext context) { - return FutureBuilder>( - future: FolderEventReadCurrentWorkspace().send(), + return FutureBuilder>( + future: FolderEventGetCurrentWorkspace().send(), builder: (context, snapshot) { if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { - final workspace = snapshot.data?.fold((s) => s, (e) => null); - if (workspace != null) { - final views = workspace.views; + final workspaces = snapshot.data?.getLeftOrNull(); + if (workspaces != null) { + final views = workspaces.workspace.views; cubit ??= SettingsFileExporterCubit(views: views); return BlocProvider.value( value: cubit!, @@ -66,20 +62,19 @@ class _FileExporterWidgetState extends State { .every((element) => element) ? LocaleKeys.settings_files_deselectAll.tr() : LocaleKeys.settings_files_selectAll.tr(), - fontColor: AFThemeExtension.of(context).textColor, onPressed: () { context .read() .selectOrDeselectAllItems(); }, ), - ), + ) ], ), const VSpace(8), const Expanded(child: _ExpandedList()), const VSpace(8), - _buildButtons(), + _buildButtons() ], ), ); @@ -95,14 +90,14 @@ class _FileExporterWidgetState extends State { children: [ const Spacer(), FlowyTextButton( - LocaleKeys.button_cancel.tr(), - fontColor: AFThemeExtension.of(context).textColor, - onPressed: () => Navigator.of(context).pop(), + LocaleKeys.button_Cancel.tr(), + onPressed: () { + Navigator.of(context).pop(); + }, ), const HSpace(8), FlowyTextButton( - LocaleKeys.button_ok.tr(), - fontColor: AFThemeExtension.of(context).textColor, + LocaleKeys.button_OK.tr(), onPressed: () async { await getIt() .getDirectoryPath() @@ -111,29 +106,22 @@ class _FileExporterWidgetState extends State { final views = cubit!.state.selectedViews; final result = await _AppFlowyFileExporter.exportToPath(exportPath, views); - if (mounted) { - if (result.$1) { - // success - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileSuccess.tr(), - ); - } else { - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileFail.tr() + - result.$2.join('\n'), - ); - } + if (result.$1) { + // success + _showToast(LocaleKeys.settings_files_exportFileSuccess.tr()); + } else { + _showToast( + LocaleKeys.settings_files_exportFileFail.tr() + + result.$2.join('\n'), + ); } - } else if (mounted) { - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileFail.tr(), - ); + } else { + _showToast(LocaleKeys.settings_files_exportFileFail.tr()); } if (mounted) { - context.popToHome(); + Navigator.of(context).popUntil( + (router) => router.settings.name == '/', + ); } }); }, @@ -141,10 +129,25 @@ class _FileExporterWidgetState extends State { ], ); } + + void _showToast(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: FlowyText( + message, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } } class _ExpandedList extends StatefulWidget { - const _ExpandedList(); + const _ExpandedList({ + Key? key, + // required this.apps, + // required this.onChanged, + }) : super(key: key); // final List apps; // final void Function(Map> selectedPages) onChanged; @@ -225,6 +228,17 @@ class _ExpandedListState extends State<_ExpandedList> { } } +extension AppFlowy on dartz.Either { + T? getLeftOrNull() { + if (isLeft()) { + final result = fold((l) => l, (r) => null); + return result; + } + + return null; + } +} + class _AppFlowyFileExporter { static Future<(bool result, List failedNames)> exportToPath( String path, @@ -241,12 +255,9 @@ class _AppFlowyFileExporter { final result = await documentExporter.export( DocumentExportType.json, ); - result.fold( - (json) { - content = json; - }, - (e) => Log.error(e), - ); + result.fold((l) => Log.error(l), (json) { + content = json; + }); fileExtension = 'afdocument'; break; default: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart new file mode 100644 index 0000000000..fe13da5360 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart @@ -0,0 +1,31 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/settings_export_file_widget.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class SettingsFileSystemView extends StatefulWidget { + const SettingsFileSystemView({ + super.key, + }); + + @override + State createState() => _SettingsFileSystemViewState(); +} + +class _SettingsFileSystemViewState extends State { + late final _items = [ + const SettingsFileLocationCustomizer(), + // disable export data for v0.2.0 in release mode. + if (kDebugMode) const SettingsExportFileWidget() + ]; + + @override + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => _items[index], + separatorBuilder: (context, index) => const Divider(), + itemCount: _items.length, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart new file mode 100644 index 0000000000..78bf071d0b --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -0,0 +1,113 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/appearance.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flowy_infra/language.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsLanguageView extends StatelessWidget { + const SettingsLanguageView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) => Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys.settings_menu_language.tr(), + ), + ), + LanguageSelector(currentLocale: state.locale), + ], + ), + ), + ); + } +} + +class LanguageSelector extends StatelessWidget { + final Locale currentLocale; + const LanguageSelector({ + super.key, + required this.currentLocale, + }); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.bottomWithRightAligned, + child: FlowyTextButton( + languageFromLocale(currentLocale), + fontColor: Theme.of(context).colorScheme.onBackground, + fillColor: Colors.transparent, + onPressed: () {}, + ), + popupBuilder: (BuildContext context) { + final allLocales = EasyLocalization.of(context)!.supportedLocales; + return LanguageItemsListView( + allLocales: allLocales, + ); + }, + ); + } +} + +class LanguageItemsListView extends StatelessWidget { + const LanguageItemsListView({ + super.key, + required this.allLocales, + }); + + final List allLocales; + + @override + Widget build(BuildContext context) { + // get current locale from cubit + final state = context.watch().state; + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 400), + child: ListView.builder( + itemBuilder: (context, index) { + final locale = allLocales[index]; + return LanguageItem(locale: locale, currentLocale: state.locale); + }, + itemCount: allLocales.length, + ), + ); + } +} + +class LanguageItem extends StatelessWidget { + final Locale locale; + final Locale currentLocale; + const LanguageItem({ + super.key, + required this.locale, + required this.currentLocale, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + child: FlowyButton( + text: FlowyText.medium( + languageFromLocale(locale), + ), + rightIcon: currentLocale == locale + ? const FlowySvg(name: 'grid/checkmark') + : null, + onTap: () { + if (currentLocale != locale) { + context.read().setLocale(context, locale); + } + }, + ), + ); + } +} 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 979f19fbde..ecdb4c6696 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,204 +1,59 @@ -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/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({ - super.key, + Key? key, required this.changeSelectedPage, required this.currentPage, - required this.userProfile, - required this.isBillingEnabled, - }); + }) : super(key: key); final Function changeSelectedPage; final SettingsPage currentPage; - final UserProfilePB userProfile; - final bool isBillingEnabled; - - @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_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.workspaceAuthType == AuthTypePB.Server) - 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: currentPage, - label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: const Icon(Icons.sync), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.shortcuts, - selectedPage: currentPage, - label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_shortcuts_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.ai, - selectedPage: currentPage, - label: LocaleKeys.settings_aiPage_menuLabel.tr(), - icon: const FlowySvg( - FlowySvgs.ai_summary_generate_s, - 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 Icon(Icons.web), - changeSelectedPage: changeSelectedPage, - ), - if (FeatureFlag.planBilling.isOn && isBillingEnabled) ...[ - 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: 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( - 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 BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: 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.cloud, - selectedPage: SettingsPage.cloud, - label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: const Icon(Icons.sync), - changeSelectedPage: () {}, - ), - if (kDebugMode) - SettingsMenuElement( - // no need to translate this page - page: SettingsPage.featureFlags, - selectedPage: SettingsPage.cloud, - label: 'Feature Flags', - icon: const Icon(Icons.flag), - changeSelectedPage: () {}, - ), - ], - ), - ), - ), + SettingsMenuElement( + page: SettingsPage.appearance, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_appearance.tr(), + icon: Icons.brightness_4, + changeSelectedPage: changeSelectedPage, + ), + const SizedBox( + height: 10, + ), + SettingsMenuElement( + page: SettingsPage.language, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_language.tr(), + icon: Icons.translate, + changeSelectedPage: changeSelectedPage, + ), + const SizedBox( + height: 10, + ), + SettingsMenuElement( + page: SettingsPage.files, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_files.tr(), + icon: Icons.file_present_outlined, + changeSelectedPage: changeSelectedPage, + ), + const SizedBox( + height: 10, + ), + SettingsMenuElement( + page: SettingsPage.user, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_user.tr(), + icon: Icons.account_box_outlined, + changeSelectedPage: changeSelectedPage, ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart index b1bef7cceb..46a11cabcb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart @@ -1,43 +1,42 @@ import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.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:flutter/material.dart'; class SettingsMenuElement extends StatelessWidget { const SettingsMenuElement({ - super.key, + Key? key, required this.page, required this.label, required this.icon, required this.changeSelectedPage, required this.selectedPage, - }); + }) : super(key: key); final SettingsPage page; final SettingsPage selectedPage; final String label; - final Widget icon; + final IconData icon; final Function changeSelectedPage; @override Widget build(BuildContext context) { return FlowyHover( - isSelected: () => page == selectedPage, - resetHoverOnRebuild: false, style: HoverStyle( - hoverColor: AFThemeExtension.of(context).greyHover, - borderRadius: BorderRadius.circular(4), + hoverColor: Theme.of(context).colorScheme.primary, ), - builder: (_, isHovering) => ListTile( - dense: true, - leading: iconWidget( - isHovering || page == selectedPage + child: ListTile( + leading: Icon( + icon, + size: 16, + color: page == selectedPage ? Theme.of(context).colorScheme.onSurface - : AFThemeExtension.of(context).textColor, + : null, ), - onTap: () => changeSelectedPage(page), + onTap: () { + changeSelectedPage(page); + }, selected: page == selectedPage, selectedColor: Theme.of(context).colorScheme.onSurface, selectedTileColor: Theme.of(context).colorScheme.primary, @@ -45,7 +44,7 @@ class SettingsMenuElement extends StatelessWidget { borderRadius: BorderRadius.circular(5), ), minLeadingWidth: 0, - title: FlowyText.medium( + title: FlowyText.semibold( label, fontSize: FontSizes.s14, overflow: TextOverflow.ellipsis, @@ -56,9 +55,4 @@ class SettingsMenuElement extends StatelessWidget { ), ); } - - Widget iconWidget(Color color) => IconTheme( - data: IconThemeData(color: color), - child: icon, - ); } 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 deleted file mode 100644 index 29ba2baf5c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; - -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'; - -class SettingsNotificationsView extends StatelessWidget { - const SettingsNotificationsView({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return SettingsBody( - title: LocaleKeys.settings_menu_notifications.tr(), - children: [ - SettingListTile( - label: LocaleKeys.settings_notifications_enableNotifications_label - .tr(), - hint: LocaleKeys.settings_notifications_enableNotifications_hint - .tr(), - trailing: [ - Toggle( - value: state.isNotificationsEnabled, - onChanged: (_) => context - .read() - .toggleNotificationsEnabled(), - ), - ], - ), - SettingListTile( - label: LocaleKeys - .settings_notifications_showNotificationsIcon_label - .tr(), - hint: LocaleKeys.settings_notifications_showNotificationsIcon_hint - .tr(), - trailing: [ - Toggle( - value: state.isShowNotificationsIconEnabled, - onChanged: (_) => context - .read() - .toogleShowNotificationIconEnabled(), - ), - ], - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart new file mode 100644 index 0000000000..a2fd1f5f02 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -0,0 +1,324 @@ +import 'dart:convert'; +import 'dart:async'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/debounce.dart'; +import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.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'; + +const defaultUserAvatar = '1F600'; +const _iconSize = Size(60, 60); + +class SettingsUserView extends StatelessWidget { + final UserProfilePB user; + SettingsUserView(this.user, {Key? key}) : super(key: ValueKey(user.id)); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(param1: user) + ..add(const SettingsUserEvent.initial()), + child: BlocBuilder( + builder: (context, state) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _renderUserNameInput(context), + const VSpace(20), + _renderCurrentIcon(context), + const VSpace(20), + _renderCurrentOpenaiKey(context) + ], + ), + ), + ), + ); + } + + Widget _renderUserNameInput(BuildContext context) { + final String name = context.read().state.userProfile.name; + return UserNameInput(name); + } + + Widget _renderCurrentIcon(BuildContext context) { + String iconUrl = + context.read().state.userProfile.iconUrl; + if (iconUrl.isEmpty) { + iconUrl = defaultUserAvatar; + } + return _CurrentIcon(iconUrl); + } + + Widget _renderCurrentOpenaiKey(BuildContext context) { + final String openAIKey = + context.read().state.userProfile.openaiKey; + return _OpenaiKeyInput(openAIKey); + } +} + +@visibleForTesting +class UserNameInput extends StatefulWidget { + final String name; + + const UserNameInput( + this.name, { + Key? key, + }) : super(key: key); + + @override + UserNameInputState createState() => UserNameInputState(); +} + +class UserNameInputState extends State { + late TextEditingController _controller; + + Timer? _debounce; + final Duration _debounceDuration = const Duration(milliseconds: 500); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.name); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: _controller, + decoration: InputDecoration( + labelText: LocaleKeys.settings_user_name.tr(), + labelStyle: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.w500), + enabledBorder: UnderlineInputBorder( + borderSide: + BorderSide(color: Theme.of(context).colorScheme.onBackground), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), + ), + ), + onChanged: (val) { + if (_debounce?.isActive ?? false) { + _debounce!.cancel(); + } + + _debounce = Timer(_debounceDuration, () { + context + .read() + .add(SettingsUserEvent.updateUserName(val)); + }); + }, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +class _OpenaiKeyInput extends StatefulWidget { + final String openAIKey; + const _OpenaiKeyInput( + this.openAIKey, { + Key? key, + }) : super(key: key); + + @override + State<_OpenaiKeyInput> createState() => _OpenaiKeyInputState(); +} + +class _OpenaiKeyInputState extends State<_OpenaiKeyInput> { + bool visible = false; + final textEditingController = TextEditingController(); + final debounce = Debounce(); + + @override + void initState() { + super.initState(); + + textEditingController.text = widget.openAIKey; + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: textEditingController, + obscureText: !visible, + decoration: InputDecoration( + enabledBorder: UnderlineInputBorder( + borderSide: + BorderSide(color: Theme.of(context).colorScheme.onBackground), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), + ), + labelText: 'OpenAI Key', + labelStyle: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.w500), + hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(), + suffixIcon: FlowyIconButton( + width: 40, + height: 40, + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + icon: Icon( + visible ? Icons.visibility : Icons.visibility_off, + ), + onPressed: () { + setState(() { + visible = !visible; + }); + }, + ), + ), + onChanged: (value) { + debounce.call(() { + context + .read() + .add(SettingsUserEvent.updateUserOpenAIKey(value)); + }); + }, + ); + } + + @override + void dispose() { + debounce.dispose(); + super.dispose(); + } +} + +class _CurrentIcon extends StatelessWidget { + final String iconUrl; + const _CurrentIcon(this.iconUrl, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + void setIcon(String iconUrl) { + context + .read() + .add(SettingsUserEvent.updateUserIcon(iconUrl)); + Navigator.of(context).pop(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.settings_user_icon.tr(), + style: Theme.of(context).textTheme.titleSmall!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 13, + ), + ), + InkWell( + borderRadius: Corners.s6Border, + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: FlowyText.medium( + LocaleKeys.settings_user_selectAnIcon.tr(), + fontSize: FontSizes.s16, + ), + children: [ + SizedBox( + height: 300, + width: 300, + child: IconGallery(setIcon), + ) + ], + ); + }, + ); + }, + child: Container( + margin: const EdgeInsets.fromLTRB(0, 5, 5, 5), + child: svgWidget( + 'emoji/$iconUrl', + size: _iconSize, + ), + ), + ), + ], + ); + } +} + +class IconGallery extends StatelessWidget { + final Function setIcon; + const IconGallery(this.setIcon, {Key? key}) : super(key: key); + + Future> _getIcons(BuildContext context) async { + final manifestContent = + await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); + + final Map manifestMap = json.decode(manifestContent); + + final iconUrls = manifestMap.keys + .where( + (String key) => + key.startsWith('assets/images/emoji/') && key.endsWith('.svg'), + ) + .map((String key) => key.split('/').last.split('.').first) + .toList(); + + return iconUrls; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _getIcons(context), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return GridView.count( + padding: const EdgeInsets.all(20), + crossAxisCount: 5, + children: (snapshot.data ?? []).map((String iconUrl) { + return IconOption(iconUrl, setIcon); + }).toList(), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + } +} + +class IconOption extends StatelessWidget { + final String iconUrl; + final Function setIcon; + + IconOption(this.iconUrl, this.setIcon, {Key? key}) + : super(key: ValueKey(iconUrl)); + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: Corners.s6Border, + hoverColor: Theme.of(context).colorScheme.tertiaryContainer, + onTap: () { + setIcon(iconUrl); + }, + child: svgWidget('emoji/$iconUrl', size: _iconSize), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_confirm_delete_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_confirm_delete_dialog.dart deleted file mode 100644 index a48996af18..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_confirm_delete_dialog.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -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/material.dart'; - -import 'theme_upload_view.dart'; - -class ThemeConfirmDeleteDialog extends StatelessWidget { - const ThemeConfirmDeleteDialog({ - super.key, - required this.theme, - }); - - final AppTheme theme; - - void onConfirm(BuildContext context) => Navigator.of(context).pop(true); - void onCancel(BuildContext context) => Navigator.of(context).pop(false); - - @override - Widget build(BuildContext context) { - return FlowyDialog( - padding: EdgeInsets.zero, - constraints: const BoxConstraints.tightFor( - width: 300, - height: 100, - ), - title: FlowyText.regular( - LocaleKeys.document_plugins_cover_alertDialogConfirmation.tr(), - textAlign: TextAlign.center, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - width: ThemeUploadWidget.buttonSize.width, - child: FlowyButton( - text: FlowyText.semibold( - LocaleKeys.button_ok.tr(), - fontSize: ThemeUploadWidget.buttonFontSize, - ), - onTap: () => onConfirm(context), - ), - ), - SizedBox( - width: ThemeUploadWidget.buttonSize.width, - child: FlowyButton( - text: FlowyText.semibold( - LocaleKeys.button_cancel.tr(), - fontSize: ThemeUploadWidget.buttonFontSize, - ), - onTap: () => onCancel(context), - ), - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart deleted file mode 100644 index 07a9b19273..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'theme_confirm_delete_dialog.dart'; -export 'theme_upload_button.dart'; -export 'theme_upload_learn_more_button.dart'; -export 'theme_upload_decoration.dart'; -export 'theme_upload_failure_widget.dart'; -export 'theme_upload_loading_widget.dart'; -export 'theme_upload_view.dart'; -export 'upload_new_theme_widget.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart deleted file mode 100644 index 73fb23b806..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; -import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'theme_upload_view.dart'; - -class ThemeUploadButton extends StatelessWidget { - const ThemeUploadButton({super.key, this.color}); - - final Color? color; - - @override - Widget build(BuildContext context) { - return SizedBox.fromSize( - size: ThemeUploadWidget.buttonSize, - child: FlowyButton( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: color ?? Theme.of(context).colorScheme.primary, - ), - hoverColor: color, - text: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText.medium( - fontSize: ThemeUploadWidget.buttonFontSize, - color: Theme.of(context).colorScheme.onPrimary, - LocaleKeys.settings_appearance_themeUpload_button.tr(), - ), - ], - ), - onTap: () => BlocProvider.of(context) - .add(DynamicPluginEvent.addPlugin()), - ), - ); - } -} 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 deleted file mode 100644 index ea8ebfe36b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:dotted_border/dotted_border.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; - -import 'theme_upload_view.dart'; - -class ThemeUploadDecoration extends StatelessWidget { - const ThemeUploadDecoration({super.key, required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: AFThemeExtension.of(context).onBackground.withValues( - alpha: ThemeUploadWidget.fadeOpacity, - ), - ), - ), - padding: ThemeUploadWidget.padding, - child: DottedBorder( - borderType: BorderType.RRect, - dashPattern: const [6, 6], - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: ThemeUploadWidget.fadeOpacity), - radius: const Radius.circular(ThemeUploadWidget.borderRadius), - child: ClipRRect( - borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), - child: child, - ), - ), - ); - } -} 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 deleted file mode 100644 index a7286bee48..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -class ThemeUploadFailureWidget extends StatelessWidget { - const ThemeUploadFailureWidget({super.key, required this.errorMessage}); - - final String errorMessage; - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context) - .colorScheme - .error - .withValues(alpha: ThemeUploadWidget.fadeOpacity), - constraints: const BoxConstraints.expand(), - padding: ThemeUploadWidget.padding, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - FlowySvg( - FlowySvgs.close_m, - size: ThemeUploadWidget.iconSize, - color: AFThemeExtension.of(context).onBackground, - ), - FlowyText.medium( - errorMessage, - overflow: TextOverflow.ellipsis, - ), - ThemeUploadWidget.elementSpacer, - const ThemeUploadLearnMoreButton(), - ThemeUploadWidget.elementSpacer, - ThemeUploadButton(color: Theme.of(context).colorScheme.error), - ThemeUploadWidget.elementSpacer, - ], - ), - ); - } -} 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 deleted file mode 100644 index bdc5ef0546..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart +++ /dev/null @@ -1,62 +0,0 @@ -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'; - -class ThemeUploadLearnMoreButton extends StatelessWidget { - const ThemeUploadLearnMoreButton({super.key}); - - static const learnMoreURL = - 'https://docs.appflowy.io/docs/appflowy/product/themes'; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: ThemeUploadWidget.buttonSize.height, - child: IntrinsicWidth( - child: SecondaryButton( - outlineColor: AFThemeExtension.of(context).onBackground, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: FlowyText.medium( - fontSize: ThemeUploadWidget.buttonFontSize, - LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), - ), - ), - onPressed: () async { - final uri = Uri.parse(learnMoreURL); - await afLaunchUri( - uri, - context: context, - onFailure: (_) async { - if (context.mounted) { - await Dialogs.show( - context, - child: FlowyDialog( - child: FlowyErrorPage.message( - LocaleKeys - .settings_appearance_themeUpload_urlUploadFailure - .tr() - .replaceAll( - '{}', - uri.toString(), - ), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), - ), - ), - ); - } - }, - ); - }, - ), - ), - ); - } -} 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 deleted file mode 100644 index 1d3e7ab0f8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class ThemeUploadLoadingWidget extends StatelessWidget { - const ThemeUploadLoadingWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - padding: ThemeUploadWidget.padding, - color: Theme.of(context) - .colorScheme - .surface - .withValues(alpha: ThemeUploadWidget.fadeOpacity), - constraints: const BoxConstraints.expand(), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - color: Theme.of(context).colorScheme.primary, - ), - ThemeUploadWidget.elementSpacer, - FlowyText.regular( - LocaleKeys.settings_appearance_themeUpload_loading.tr(), - ), - ], - ), - ); - } -} 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 deleted file mode 100644 index 1b22dba659..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart +++ /dev/null @@ -1,80 +0,0 @@ -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_bloc/flutter_bloc.dart'; - -import 'theme_upload_decoration.dart'; -import 'theme_upload_failure_widget.dart'; -import 'theme_upload_loading_widget.dart'; -import 'upload_new_theme_widget.dart'; - -class ThemeUploadWidget extends StatefulWidget { - const ThemeUploadWidget({super.key}); - - static const double borderRadius = 8; - static const double buttonFontSize = 14; - static const Size buttonSize = Size(100, 32); - static const EdgeInsets padding = EdgeInsets.all(12.0); - static const Size iconSize = Size.square(48); - static const Widget elementSpacer = SizedBox(height: 12); - static const double fadeOpacity = 0.5; - static const Duration fadeDuration = Duration(milliseconds: 750); - - @override - State createState() => _ThemeUploadWidgetState(); -} - -class _ThemeUploadWidgetState extends State { - void listen(BuildContext context, DynamicPluginState state) { - setState(() { - state.whenOrNull( - ready: (plugins) { - child = - const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); - }, - deletionSuccess: () { - child = - const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); - }, - processing: () { - child = const ThemeUploadLoadingWidget( - key: Key('upload_theme_loading_widget'), - ); - }, - compilationFailure: (errorMessage) { - child = ThemeUploadFailureWidget( - key: const Key('upload_theme_failure_widget'), - errorMessage: errorMessage, - ); - }, - compilationSuccess: () { - if (Navigator.of(context).canPop()) { - Navigator.of(context) - .pop(const DynamicPluginState.compilationSuccess()); - } - }, - ); - }); - } - - Widget child = const UploadNewThemeWidget( - key: Key('upload_new_theme_widget'), - ); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: listen, - child: ThemeUploadDecoration( - child: Center( - child: AnimatedSwitcher( - duration: ThemeUploadWidget.fadeDuration, - switchInCurve: Curves.easeInOutCubicEmphasized, - child: child, - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index 02a7c8e7ab..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.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'; - -class UploadNewThemeWidget extends StatelessWidget { - const UploadNewThemeWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context) - .colorScheme - .surface - .withValues(alpha: ThemeUploadWidget.fadeOpacity), - padding: ThemeUploadWidget.padding, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(), - FlowySvg( - FlowySvgs.folder_m, - size: ThemeUploadWidget.iconSize, - color: AFThemeExtension.of(context).onBackground, - ), - FlowyText.medium( - LocaleKeys.settings_appearance_themeUpload_description.tr(), - overflow: TextOverflow.ellipsis, - ), - ThemeUploadWidget.elementSpacer, - const ThemeUploadLearnMoreButton(), - ThemeUploadWidget.elementSpacer, - const Divider(), - ThemeUploadWidget.elementSpacer, - const ThemeUploadButton(), - ThemeUploadWidget.elementSpacer, - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart deleted file mode 100644 index 715b88f279..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart +++ /dev/null @@ -1,19 +0,0 @@ -enum FormFactor { - mobile._(600), - tablet._(840), - desktop._(1280); - - const FormFactor._(this.width); - - factory FormFactor.fromWidth(double width) { - if (width < FormFactor.mobile.width) { - return FormFactor.mobile; - } else if (width < FormFactor.tablet.width) { - return FormFactor.tablet; - } else { - return FormFactor.desktop; - } - } - - final double width; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart deleted file mode 100644 index 537de2768c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart +++ /dev/null @@ -1,19 +0,0 @@ -extension HexOpacityExtension on String { - /// Only used in a valid color String like '0xff00bcf0' - String extractHex() { - return substring(4); - } - - /// Only used in a valid color String like '0xff00bcf0' - String extractOpacity() { - final opacityString = substring(2, 4); - final opacityInt = int.parse(opacityString, radix: 16) / 2.55; - return opacityInt.toStringAsFixed(0); - } - - /// Apply on the hex string like '00bcf0', with opacity like '100' - String combineHexWithOpacity(String opacity) { - final opacityInt = (int.parse(opacity) * 2.55).round().toRadixString(16); - return '0x$opacityInt$this'; - } -} 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_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_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/utils/date_time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart deleted file mode 100644 index 36657a4321..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; - -extension ToDateFormat on UserDateFormatPB { - DateFormatPB get simplified => switch (this) { - UserDateFormatPB.DayMonthYear => DateFormatPB.DayMonthYear, - UserDateFormatPB.Friendly => DateFormatPB.Friendly, - UserDateFormatPB.ISO => DateFormatPB.ISO, - UserDateFormatPB.Locally => DateFormatPB.Local, - UserDateFormatPB.US => DateFormatPB.US, - _ => DateFormatPB.Friendly, - }; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/layout.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/layout.dart deleted file mode 100644 index 7dde3368f2..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/layout.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class DatePickerSize { - static double scale = 1; - - static double get itemHeight => 26 * scale; - static double get seperatorHeight => 4 * scale; - - static EdgeInsets get itemOptionInsets => const EdgeInsets.all(4); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart deleted file mode 100644 index 2040785371..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; - -extension ToTimeFormat on UserTimeFormatPB { - TimeFormatPB get simplified => switch (this) { - UserTimeFormatPB.TwelveHour => TimeFormatPB.TwelveHour, - UserTimeFormatPB.TwentyFourHour => TimeFormatPB.TwentyFourHour, - _ => TimeFormatPB.TwentyFourHour, - }; -} 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 deleted file mode 100644 index 3eaa674df8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; -import 'package:flutter/material.dart'; - -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/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; - -class ClearDateButton extends StatelessWidget { - const ClearDateButton({ - super.key, - required this.onClearDate, - }); - - final VoidCallback onClearDate; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: DatePickerSize.itemHeight, - child: FlowyButton( - text: FlowyText(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 deleted file mode 100644 index 5cbdc2bc43..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart +++ /dev/null @@ -1,191 +0,0 @@ -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); - -class DatePicker extends StatefulWidget { - const DatePicker({ - super.key, - required this.isRange, - this.calendarFormat = CalendarFormat.month, - this.startDay, - this.endDay, - this.selectedDay, - required this.focusedDay, - this.onDaySelected, - this.onRangeSelected, - this.onCalendarCreated, - this.onPageChanged, - }); - - final bool isRange; - final CalendarFormat calendarFormat; - - final DateTime? startDay; - final DateTime? endDay; - final DateTime? selectedDay; - - final DateTime focusedDay; - - final void Function( - DateTime selectedDay, - DateTime focusedDay, - )? onDaySelected; - - final void Function( - DateTime? start, - DateTime? end, - DateTime focusedDay, - )? onRangeSelected; - - final void Function(PageController pageController)? onCalendarCreated; - - final void Function(DateTime focusedDay)? onPageChanged; - - @override - State createState() => _DatePickerState(); -} - -class _DatePickerState extends State { - late CalendarFormat _calendarFormat = widget.calendarFormat; - - @override - Widget build(BuildContext context) { - final textStyle = Theme.of(context).textTheme.bodyMedium!; - final boxDecoration = BoxDecoration( - color: Theme.of(context).cardColor, - shape: BoxShape.circle, - ); - - final calendarStyle = UniversalPlatform.isMobile - ? _CalendarStyle.mobile( - dowTextStyle: textStyle.copyWith( - color: Theme.of(context).hintColor, - fontSize: 14.0, - ), - ) - : _CalendarStyle.desktop( - dowTextStyle: AFThemeExtension.of(context).caption, - selectedColor: Theme.of(context).colorScheme.primary, - ); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TableCalendar( - firstDay: kFirstDay, - lastDay: kLastDay, - focusedDay: widget.focusedDay, - rowHeight: calendarStyle.rowHeight, - calendarFormat: _calendarFormat, - daysOfWeekHeight: calendarStyle.dowHeight, - rangeSelectionMode: widget.isRange - ? RangeSelectionMode.enforced - : RangeSelectionMode.disabled, - rangeStartDay: widget.isRange ? widget.startDay : null, - rangeEndDay: widget.isRange ? widget.endDay : null, - availableGestures: calendarStyle.availableGestures, - availableCalendarFormats: const {CalendarFormat.month: 'Month'}, - onCalendarCreated: widget.onCalendarCreated, - headerVisible: calendarStyle.headerVisible, - headerStyle: calendarStyle.headerStyle, - calendarStyle: CalendarStyle( - cellMargin: const EdgeInsets.all(3.5), - defaultDecoration: boxDecoration, - selectedDecoration: boxDecoration.copyWith( - color: calendarStyle.selectedColor, - ), - todayDecoration: boxDecoration.copyWith( - color: Colors.transparent, - border: Border.all(color: calendarStyle.selectedColor), - ), - weekendDecoration: boxDecoration, - outsideDecoration: boxDecoration, - rangeStartDecoration: boxDecoration.copyWith( - color: calendarStyle.selectedColor, - ), - rangeEndDecoration: boxDecoration.copyWith( - color: calendarStyle.selectedColor, - ), - defaultTextStyle: textStyle, - weekendTextStyle: textStyle, - selectedTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - rangeStartTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - rangeEndTextStyle: textStyle.copyWith( - color: Theme.of(context).colorScheme.surface, - ), - todayTextStyle: textStyle, - outsideTextStyle: textStyle.copyWith( - color: Theme.of(context).disabledColor, - ), - rangeHighlightColor: Theme.of(context).colorScheme.secondaryContainer, - ), - calendarBuilders: CalendarBuilders( - dowBuilder: (context, day) { - final locale = context.locale.toLanguageTag(); - final label = DateFormat.E(locale).format(day); - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Center( - child: Text(label, style: calendarStyle.dowTextStyle), - ), - ); - }, - ), - selectedDayPredicate: (day) => - widget.isRange ? false : isSameDay(widget.selectedDay, day), - onFormatChanged: (calendarFormat) => - setState(() => _calendarFormat = calendarFormat), - onPageChanged: (focusedDay) { - widget.onPageChanged?.call(focusedDay); - }, - onDaySelected: widget.onDaySelected, - onRangeSelected: widget.onRangeSelected, - ), - ); - } -} - -class _CalendarStyle { - _CalendarStyle.desktop({ - required this.selectedColor, - required this.dowTextStyle, - }) : rowHeight = 33, - dowHeight = 35, - headerVisible = false, - headerStyle = const HeaderStyle(), - availableGestures = AvailableGestures.horizontalSwipe; - - _CalendarStyle.mobile({required this.dowTextStyle}) - : rowHeight = 48, - dowHeight = 48, - headerVisible = false, - headerStyle = const HeaderStyle(), - selectedColor = const Color(0xFF00BCF0), - availableGestures = AvailableGestures.horizontalSwipe; - - _CalendarStyle({ - required this.rowHeight, - required this.dowHeight, - required this.headerVisible, - required this.headerStyle, - required this.dowTextStyle, - required this.selectedColor, - required this.availableGestures, - }); - - final double rowHeight; - final double dowHeight; - final bool headerVisible; - final HeaderStyle headerStyle; - final TextStyle dowTextStyle; - final Color selectedColor; - final AvailableGestures availableGestures; -} 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 deleted file mode 100644 index 54fc2fac2a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ /dev/null @@ -1,185 +0,0 @@ -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/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'; -import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/style_widget/decoration.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -/// Provides arguemnts for [AppFlowyDatePicker] when showing -/// a [DatePickerMenu] -/// -class DatePickerOptions { - DatePickerOptions({ - DateTime? focusedDay, - this.selectedDay, - this.includeTime = false, - this.isRange = false, - this.dateFormat = UserDateFormatPB.Friendly, - this.timeFormat = UserTimeFormatPB.TwentyFourHour, - this.selectedReminderOption, - this.onDaySelected, - this.onIncludeTimeChanged, - this.onRangeSelected, - this.onIsRangeChanged, - this.onReminderSelected, - }) : focusedDay = focusedDay ?? DateTime.now(); - - final DateTime focusedDay; - final DateTime? selectedDay; - final bool includeTime; - final bool isRange; - final UserDateFormatPB dateFormat; - final UserTimeFormatPB timeFormat; - final ReminderOption? selectedReminderOption; - - final DaySelectedCallback? onDaySelected; - final RangeSelectedCallback? onRangeSelected; - final IncludeTimeChangedCallback? onIncludeTimeChanged; - final IsRangeChangedCallback? 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 _ySpacing = 15; - -class DatePickerMenu extends DatePickerService { - DatePickerMenu({required this.context, required this.editorState}); - - final BuildContext context; - final EditorState editorState; - PopoverMutex? popoverMutex; - - OverlayEntry? _menuEntry; - - @override - void dismiss() { - _menuEntry?.remove(); - _menuEntry = null; - popoverMutex?.close(); - popoverMutex?.dispose(); - popoverMutex = null; - } - - @override - void show(Offset offset, {required DatePickerOptions options}) => - _show(offset, options: options); - - void _show(Offset offset, {required DatePickerOptions options}) { - dismiss(); - - final editorSize = editorState.renderBox!.size; - - double offsetX = offset.dx; - double offsetY = offset.dy; - - final showRight = (offset.dx + _datePickerWidth) < editorSize.width; - if (!showRight) { - offsetX = offset.dx - _datePickerWidth; - } - - final showBelow = (offset.dy + _datePickerHeight) < editorSize.height; - if (!showBelow) { - if ((offset.dy - _datePickerHeight) < 0) { - // Show dialog in the middle - offsetY = offset.dy - (_datePickerHeight / 3); - } else { - // Show above - offsetY = offset.dy - _datePickerHeight; - } - } - - popoverMutex = PopoverMutex(); - _menuEntry = OverlayEntry( - builder: (_) => Material( - type: MaterialType.transparency, - child: SizedBox( - height: editorSize.height, - width: editorSize.width, - child: KeyboardListener( - focusNode: FocusNode()..requestFocus(), - onKeyEvent: (event) { - if (event.logicalKey == LogicalKeyboardKey.escape) { - dismiss(); - } - }, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: dismiss, - child: Stack( - children: [ - _AnimatedDatePicker( - offset: Offset(offsetX, offsetY), - showBelow: showBelow, - options: options, - popoverMutex: popoverMutex, - ), - ], - ), - ), - ), - ), - ), - ); - - Overlay.of(context).insert(_menuEntry!); - } -} - -class _AnimatedDatePicker extends StatelessWidget { - 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 - Widget build(BuildContext context) { - final dy = offset.dy + (showBelow ? _ySpacing : -_ySpacing); - - return AnimatedPositioned( - duration: const Duration(milliseconds: 200), - top: dy, - left: 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, - ), - ), - ); - } -} 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 deleted file mode 100644 index 7447700fef..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -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:flowy_infra_ui/flowy_infra_ui.dart'; - -class DateTimeSetting extends StatefulWidget { - const DateTimeSetting({ - super.key, - required this.dateFormat, - required this.timeFormat, - required this.onDateFormatChanged, - required this.onTimeFormatChanged, - }); - - final DateFormatPB dateFormat; - final TimeFormatPB timeFormat; - final Function(DateFormatPB) onDateFormatChanged; - final Function(TimeFormatPB) onTimeFormatChanged; - - @override - State createState() => _DateTimeSettingState(); -} - -class _DateTimeSettingState extends State { - final timeSettingPopoverMutex = PopoverMutex(); - String? overlayIdentifier; - - @override - void dispose() { - timeSettingPopoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final List children = [ - AppFlowyPopover( - mutex: timeSettingPopoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - popupBuilder: (_) => DateFormatList( - selectedFormat: widget.dateFormat, - onSelected: _onDateFormatChanged, - ), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 6.0), - child: DateFormatButton(), - ), - ), - AppFlowyPopover( - mutex: timeSettingPopoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(8, 0), - popupBuilder: (_) => TimeFormatList( - selectedFormat: widget.timeFormat, - onSelected: _onTimeFormatChanged, - ), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 6.0), - child: TimeFormatButton(), - ), - ), - ]; - - return SizedBox( - width: 180, - child: ListView.separated( - shrinkWrap: true, - separatorBuilder: (_, __) => VSpace(DatePickerSize.seperatorHeight), - itemCount: children.length, - itemBuilder: (_, int index) => children[index], - padding: const EdgeInsets.symmetric(vertical: 6.0), - ), - ); - } - - void _onTimeFormatChanged(TimeFormatPB format) { - widget.onTimeFormatChanged(format); - timeSettingPopoverMutex.close(); - } - - void _onDateFormatChanged(DateFormatPB format) { - widget.onDateFormatChanged(format); - timeSettingPopoverMutex.close(); - } -} 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 deleted file mode 100644 index 9bb819a243..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart +++ /dev/null @@ -1,60 +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: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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -class DateTypeOptionButton extends StatelessWidget { - const DateTypeOptionButton({ - super.key, - required this.dateFormat, - required this.timeFormat, - required this.onDateFormatChanged, - required this.onTimeFormatChanged, - required this.popoverMutex, - }); - - final DateFormatPB dateFormat; - final TimeFormatPB timeFormat; - final Function(DateFormatPB) onDateFormatChanged; - final Function(TimeFormatPB) onTimeFormatChanged; - final PopoverMutex? popoverMutex; - - @override - Widget build(BuildContext context) { - final title = - "${LocaleKeys.datePicker_dateFormat.tr()} & ${LocaleKeys.datePicker_timeFormat.tr()}"; - return AppFlowyPopover( - mutex: popoverMutex, - offset: const Offset(8, 0), - margin: EdgeInsets.zero, - constraints: BoxConstraints.loose(const Size(140, 100)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText(title), - rightIcon: const FlowySvg(FlowySvgs.more_s), - ), - ), - ), - popupBuilder: (_) => DateTimeSetting( - dateFormat: dateFormat, - timeFormat: timeFormat, - onDateFormatChanged: (format) { - onDateFormatChanged(format); - popoverMutex?.close(); - }, - onTimeFormatChanged: (format) { - onTimeFormatChanged(format); - popoverMutex?.close(); - }, - ), - ); - } -} 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 deleted file mode 100644 index fdb24fb761..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart +++ /dev/null @@ -1,49 +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:appflowy/plugins/database/grid/presentation/layout/sizes.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.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; - -class EndTimeButton extends StatelessWidget { - const EndTimeButton({ - super.key, - required this.isRange, - required this.onChanged, - }); - - final bool isRange; - final Function(bool value) onChanged; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: GridSize.typeOptionContentInsets, - child: Row( - children: [ - FlowySvg( - FlowySvgs.date_s, - color: Theme.of(context).iconTheme.color, - ), - const HSpace(6), - FlowyText(LocaleKeys.datePicker_isRange.tr()), - const Spacer(), - Toggle( - value: isRange, - onChanged: onChanged, - 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 deleted file mode 100644 index 4d8176ba5c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.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'; - -class MobileDatePicker extends StatefulWidget { - const MobileDatePicker({ - super.key, - this.selectedDay, - this.startDay, - this.endDay, - required this.focusedDay, - required this.isRange, - this.onDaySelected, - this.onRangeSelected, - this.onPageChanged, - }); - - 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; - - @override - State createState() => _MobileDatePickerState(); -} - -class _MobileDatePickerState extends State { - PageController? pageController; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const VSpace(8.0), - _buildHeader(context), - const VSpace(8.0), - _buildCalendar(context), - const VSpace(16.0), - ], - ); - } - - Widget _buildCalendar(BuildContext context) { - return DatePicker( - isRange: widget.isRange, - onDaySelected: (selectedDay, _) { - widget.onDaySelected?.call(selectedDay); - }, - focusedDay: widget.focusedDay, - onRangeSelected: (start, end, focusedDay) { - widget.onRangeSelected?.call(start, end); - }, - selectedDay: widget.selectedDay, - startDay: widget.startDay, - endDay: widget.endDay, - onCalendarCreated: (pageController) { - this.pageController = pageController; - }, - onPageChanged: widget.onPageChanged, - ); - } - - 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), - ), - ), - 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, - ); - }, - ), - 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, - ); - }, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart deleted file mode 100644 index 35d4d89531..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -const _height = 44.0; - -class MobileDateHeader extends StatelessWidget { - const MobileDateHeader({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).colorScheme.surface, - child: Stack( - children: [ - const Align( - alignment: Alignment.centerLeft, - child: AppBarCloseButton(), - ), - Align( - child: FlowyText.medium( - LocaleKeys.grid_field_dateFieldName.tr(), - fontSize: 16, - ), - ), - ].map((e) => SizedBox(height: _height, child: e)).toList(), - ), - ); - } -} 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 deleted file mode 100644 index d795e2ab7d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart +++ /dev/null @@ -1,204 +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:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; -import 'package:calendar_view/calendar_view.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -typedef OnReminderSelected = void Function(ReminderOption option); - -class ReminderSelector extends StatelessWidget { - const ReminderSelector({ - super.key, - required this.mutex, - required this.selectedOption, - required this.onOptionSelected, - required this.timeFormat, - this.hasTime = false, - }); - - final PopoverMutex? mutex; - final ReminderOption selectedOption; - final OnReminderSelected? onOptionSelected; - final TimeFormatPB timeFormat; - final bool hasTime; - - @override - Widget build(BuildContext context) { - final options = ReminderOption.values.toList(); - if (selectedOption != ReminderOption.custom) { - options.remove(ReminderOption.custom); - } - - options.removeWhere( - (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime), - ); - - final optionWidgets = options.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 SizedBox( - height: DatePickerSize.itemHeight, - child: FlowyButton( - text: FlowyText(label), - rightIcon: - o == selectedOption ? const FlowySvg(FlowySvgs.check_s) : null, - onTap: () { - if (o != selectedOption) { - onOptionSelected?.call(o); - mutex?.close(); - } - }, - ), - ); - }, - ).toList(); - - return AppFlowyPopover( - mutex: mutex, - offset: const Offset(8, 0), - margin: EdgeInsets.zero, - constraints: const BoxConstraints(maxHeight: 400, maxWidth: 205), - popupBuilder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(6.0), - child: SeparatedColumn( - children: optionWidgets, - separatorBuilder: () => VSpace(DatePickerSize.seperatorHeight), - ), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: DatePickerSize.itemHeight, - child: FlowyButton( - text: FlowyText(LocaleKeys.datePicker_reminderLabel.tr()), - rightIcon: Row( - children: [ - FlowyText.regular(selectedOption.label), - const FlowySvg(FlowySvgs.more_s), - ], - ), - ), - ), - ), - ); - } -} - -enum ReminderOption { - none(time: Duration()), - atTimeOfEvent(time: Duration()), - fiveMinsBefore(time: Duration(minutes: 5)), - tenMinsBefore(time: Duration(minutes: 10)), - fifteenMinsBefore(time: Duration(minutes: 15)), - thirtyMinsBefore(time: Duration(minutes: 30)), - oneHourBefore(time: Duration(hours: 1)), - twoHoursBefore(time: Duration(hours: 2)), - onDayOfEvent( - time: Duration(hours: 9), - withoutTime: true, - requiresNoTime: true, - ), - // 9:00 AM the day before (24-9) - oneDayBefore(time: Duration(hours: 15), withoutTime: true), - twoDaysBefore(time: Duration(days: 1, hours: 15), withoutTime: true), - oneWeekBefore(time: Duration(days: 6, hours: 15), withoutTime: true), - custom(time: Duration()); - - const 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 => - [ReminderOption.none, ReminderOption.custom].contains(this); - - String get label => switch (this) { - ReminderOption.none => LocaleKeys.datePicker_reminderOptions_none.tr(), - ReminderOption.atTimeOfEvent => - LocaleKeys.datePicker_reminderOptions_atTimeOfEvent.tr(), - ReminderOption.fiveMinsBefore => - LocaleKeys.datePicker_reminderOptions_fiveMinsBefore.tr(), - ReminderOption.tenMinsBefore => - LocaleKeys.datePicker_reminderOptions_tenMinsBefore.tr(), - ReminderOption.fifteenMinsBefore => - LocaleKeys.datePicker_reminderOptions_fifteenMinsBefore.tr(), - ReminderOption.thirtyMinsBefore => - LocaleKeys.datePicker_reminderOptions_thirtyMinsBefore.tr(), - ReminderOption.oneHourBefore => - LocaleKeys.datePicker_reminderOptions_oneHourBefore.tr(), - ReminderOption.twoHoursBefore => - LocaleKeys.datePicker_reminderOptions_twoHoursBefore.tr(), - ReminderOption.onDayOfEvent => - LocaleKeys.datePicker_reminderOptions_onDayOfEvent.tr(), - ReminderOption.oneDayBefore => - LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(), - ReminderOption.twoDaysBefore => - LocaleKeys.datePicker_reminderOptions_twoDaysBefore.tr(), - ReminderOption.oneWeekBefore => - LocaleKeys.datePicker_reminderOptions_oneWeekBefore.tr(), - ReminderOption.custom => - LocaleKeys.datePicker_reminderOptions_custom.tr(), - }; - - static ReminderOption fromDateDifference( - DateTime eventDate, - DateTime reminderDate, - ) { - final def = fromMinutes(eventDate.difference(reminderDate).inMinutes); - if (def != ReminderOption.custom) { - return def; - } - - final diff = eventDate.withoutTime.difference(reminderDate).inMinutes; - return fromMinutes(diff); - } - - static ReminderOption fromMinutes(int minutes) => switch (minutes) { - 0 => ReminderOption.atTimeOfEvent, - 5 => ReminderOption.fiveMinsBefore, - 10 => ReminderOption.tenMinsBefore, - 15 => ReminderOption.fifteenMinsBefore, - 30 => ReminderOption.thirtyMinsBefore, - 60 => ReminderOption.oneHourBefore, - 120 => ReminderOption.twoHoursBefore, - // Negative because Event Day Today + 940 minutes - -540 => ReminderOption.onDayOfEvent, - 900 => ReminderOption.oneDayBefore, - 2340 => ReminderOption.twoDaysBefore, - 9540 => ReminderOption.oneWeekBefore, - _ => ReminderOption.custom, - }; - - DateTime getNotificationDateTime(DateTime date) { - return withoutTime - ? requiresNoTime - ? date.withoutTime.add(time) - : date.withoutTime.subtract(time) - : date.subtract(time); - } -} 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 7e30c4fa55..5b62a726bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,125 +1,41 @@ -import 'package:appflowy/generated/flowy_svgs.g.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'; -import 'package:flowy_infra_ui/style_widget/text_input.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/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'; - +import 'package:appflowy/startup/tasks/app_widget.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/text_input.dart'; +import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.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(); - }, - ), - ], - ], - ), - ); - } -} +import 'package:appflowy/generated/locale_keys.g.dart'; class NavigatorTextFieldDialog extends StatefulWidget { - const NavigatorTextFieldDialog({ - super.key, - required this.title, - this.autoSelectAllText = false, - required this.value, - required this.onConfirm, - this.onCancel, - this.maxLength, - this.hintText, - }); - final String value; final String title; - final VoidCallback? onCancel; - final void Function(String, BuildContext) onConfirm; - final bool autoSelectAllText; - final int? maxLength; - final String? hintText; + final void Function()? cancel; + final void Function(String) confirm; + + const NavigatorTextFieldDialog({ + required this.title, + required this.value, + required this.confirm, + this.cancel, + Key? key, + }) : super(key: key); @override - State createState() => - _NavigatorTextFieldDialogState(); + State createState() => _CreateTextFieldDialog(); } -class _NavigatorTextFieldDialogState extends State { +class _CreateTextFieldDialog extends State { String newValue = ""; - final controller = TextEditingController(); @override void initState() { - super.initState(); newValue = widget.value; - controller.text = newValue; - if (widget.autoSelectAllText) { - controller.selection = TextSelection( - baseOffset: 0, - extentOffset: newValue.length, - ); - } - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); + super.initState(); } @override @@ -134,41 +50,34 @@ class _NavigatorTextFieldDialogState extends State { ), VSpace(Insets.m), FlowyFormTextInput( - hintText: - widget.hintText ?? LocaleKeys.dialogCreatePageNameHint.tr(), - controller: controller, - textStyle: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(fontSize: FontSizes.s16), - maxLength: widget.maxLength, - showCounter: false, + textAlign: TextAlign.center, + hintText: LocaleKeys.dialogCreatePageNameHint.tr(), + initialValue: widget.value, + textStyle: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: FontSizes.s16, + ), autoFocus: true, onChanged: (text) { newValue = text; }, onEditingComplete: () { - widget.onConfirm(newValue, context); + widget.confirm(newValue); AppGlobals.nav.pop(); }, ), VSpace(Insets.xl), OkCancelButton( onOkPressed: () { - if (newValue.isEmpty) { - showToastNotification( - message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(), - ); - return; - } - widget.onConfirm(newValue, context); + widget.confirm(newValue); Navigator.of(context).pop(); }, onCancelPressed: () { - widget.onCancel?.call(); + if (widget.cancel != null) { + widget.cancel!(); + } Navigator.of(context).pop(); }, - ), + ) ], ), ); @@ -176,26 +85,27 @@ class _NavigatorTextFieldDialogState extends State { } class NavigatorAlertDialog extends StatefulWidget { - const NavigatorAlertDialog({ - super.key, - required this.title, - this.cancel, - this.confirm, - this.hideCancelButton = false, - this.constraints, - }); - final String title; final void Function()? cancel; final void Function()? confirm; - final bool hideCancelButton; - final BoxConstraints? constraints; + + const NavigatorAlertDialog({ + required this.title, + this.confirm, + this.cancel, + Key? key, + }) : super(key: key); @override State createState() => _CreateFlowyAlertDialog(); } class _CreateFlowyAlertDialog extends State { + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { return StyledDialog( @@ -204,19 +114,10 @@ class _CreateFlowyAlertDialog extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ ...[ - ConstrainedBox( - constraints: widget.constraints ?? - const BoxConstraints( - maxWidth: 400, - maxHeight: 260, - ), - child: FlowyText.medium( - widget.title, - fontSize: FontSizes.s16, - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.tertiary, - maxLines: null, - ), + FlowyText.medium( + widget.title, + fontSize: FontSizes.s16, + color: Theme.of(context).colorScheme.tertiary, ), ], if (widget.confirm != null) ...[ @@ -226,14 +127,12 @@ class _CreateFlowyAlertDialog extends State { widget.confirm?.call(); Navigator.of(context).pop(); }, - onCancelPressed: widget.hideCancelButton - ? null - : () { - widget.cancel?.call(); - Navigator.of(context).pop(); - }, - ), - ], + onCancelPressed: () { + widget.cancel?.call(); + Navigator.of(context).pop(); + }, + ) + ] ], ), ); @@ -241,75 +140,58 @@ class _CreateFlowyAlertDialog extends State { } class NavigatorOkCancelDialog extends StatelessWidget { - const NavigatorOkCancelDialog({ - super.key, - this.onOkPressed, - this.onCancelPressed, - this.okTitle, - this.cancelTitle, - this.title, - this.message, - this.maxWidth, - this.titleUpperCase = true, - this.autoDismiss = true, - }); - final VoidCallback? onOkPressed; final VoidCallback? onCancelPressed; final String? okTitle; final String? cancelTitle; final String? title; - final String? message; + final String message; final double? maxWidth; - final bool titleUpperCase; - final bool autoDismiss; + + const NavigatorOkCancelDialog({ + Key? key, + this.onOkPressed, + this.onCancelPressed, + this.okTitle, + this.cancelTitle, + this.title, + required this.message, + this.maxWidth, + }) : super(key: key); @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), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null) ...[ FlowyText.medium( - titleUpperCase ? title!.toUpperCase() : title!, + title!.toUpperCase(), fontSize: FontSizes.s16, - maxLines: 3, ), VSpace(Insets.sm * 1.5), Container( - color: Theme.of(context).colorScheme.surfaceContainerHighest, + color: Theme.of(context).colorScheme.surfaceVariant, height: 1, ), VSpace(Insets.m * 1.5), ], - if (message != null) - FlowyText.medium( - message!, - maxLines: 3, - ), + FlowyText.medium(message), SizedBox(height: Insets.l), 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(), - ), + ) ], ), ); @@ -317,24 +199,22 @@ class NavigatorOkCancelDialog extends StatelessWidget { } class OkCancelButton extends StatelessWidget { - const OkCancelButton({ - super.key, - this.onOkPressed, - this.onCancelPressed, - this.okTitle, - this.cancelTitle, - this.minHeight, - this.alignment = MainAxisAlignment.spaceAround, - this.mode = TextButtonMode.big, - }); - final VoidCallback? onOkPressed; final VoidCallback? onCancelPressed; final String? okTitle; final String? cancelTitle; final double? minHeight; final MainAxisAlignment alignment; - final TextButtonMode mode; + + const OkCancelButton({ + Key? key, + this.onOkPressed, + this.onCancelPressed, + this.okTitle, + this.cancelTitle, + this.minHeight, + this.alignment = MainAxisAlignment.spaceAround, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -345,394 +225,19 @@ class OkCancelButton extends StatelessWidget { children: [ if (onCancelPressed != null) SecondaryTextButton( - cancelTitle ?? LocaleKeys.button_cancel.tr(), + cancelTitle ?? LocaleKeys.button_Cancel.tr(), onPressed: onCancelPressed, - mode: mode, + mode: SecondaryTextButtonMode.big, ), - if (onCancelPressed != null) HSpace(Insets.m), + HSpace(Insets.m), if (onOkPressed != null) PrimaryTextButton( - okTitle ?? LocaleKeys.button_ok.tr(), + okTitle ?? LocaleKeys.button_OK.tr(), onPressed: onOkPressed, - mode: mode, + bigMode: true, ), ], ), ); } } - -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, -}) { - 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(), - 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 deleted file mode 100644 index 5b3962cd63..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart +++ /dev/null @@ -1,174 +0,0 @@ -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({ - super.key, - required this.child, - required this.data, - this.feedback, - this.childWhenDragging, - this.onAcceptWithDetails, - this.onWillAcceptWithDetails, - this.onMove, - this.onLeave, - this.enableAutoScroll = true, - this.hitTestSize = const Size(100, 100), - this.onDragging, - }); - - final T data; - - final Widget child; - final Widget? feedback; - final Widget? childWhenDragging; - - final DragTargetAcceptWithDetails? onAcceptWithDetails; - final DragTargetWillAcceptWithDetails? onWillAcceptWithDetails; - final DragTargetMove? onMove; - final DragTargetLeave? onLeave; - - /// Whether to enable auto scroll when dragging. - /// - /// If true, the draggable item must be wrapped inside a [Scrollable] widget. - final bool enableAutoScroll; - final Size hitTestSize; - - final void Function(bool isDragging)? onDragging; - - @override - State> createState() => _DraggableItemState(); -} - -class _DraggableItemState extends State> { - ScrollableState? scrollable; - EdgeDraggingAutoScroller? autoScroller; - Rect? dragTarget; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - initAutoScrollerIfNeeded(context); - } - - @override - Widget build(BuildContext context) { - initAutoScrollerIfNeeded(context); - - return DragTarget( - onAcceptWithDetails: widget.onAcceptWithDetails, - onWillAcceptWithDetails: widget.onWillAcceptWithDetails, - onMove: widget.onMove, - onLeave: widget.onLeave, - builder: (_, __, ___) => _Draggable( - data: widget.data, - feedback: widget.feedback ?? widget.child, - childWhenDragging: widget.childWhenDragging ?? widget.child, - child: widget.child, - onDragUpdate: (details) { - if (widget.enableAutoScroll && !disableAutoScrollWhenDragging) { - dragTarget = details.globalPosition & widget.hitTestSize; - autoScroller?.startAutoScrollIfNecessary(dragTarget!); - } - widget.onDragging?.call(true); - }, - onDragEnd: (details) { - autoScroller?.stopAutoScroll(); - dragTarget = null; - widget.onDragging?.call(false); - }, - onDraggableCanceled: (_, __) { - autoScroller?.stopAutoScroll(); - dragTarget = null; - widget.onDragging?.call(false); - }, - ), - ); - } - - void initAutoScrollerIfNeeded(BuildContext context) { - if (!widget.enableAutoScroll || disableAutoScrollWhenDragging) { - return; - } - - scrollable = Scrollable.of(context); - if (scrollable == null) { - throw FlutterError( - 'DraggableItem must be wrapped inside a Scrollable widget ' - 'when enableAutoScroll is true.', - ); - } - - autoScroller?.stopAutoScroll(); - autoScroller = EdgeDraggingAutoScroller( - scrollable!, - onScrollViewScrolled: () { - if (dragTarget != null && !disableAutoScrollWhenDragging) { - autoScroller!.startAutoScrollIfNecessary(dragTarget!); - } - }, - velocityScalar: 20, - ); - } -} - -class _Draggable extends StatelessWidget { - const _Draggable({ - required this.child, - required this.feedback, - this.data, - this.childWhenDragging, - this.onDragStarted, - this.onDragUpdate, - this.onDraggableCanceled, - this.onDragEnd, - this.onDragCompleted, - }); - - /// The data that will be dropped by this draggable. - final T? data; - - final Widget child; - - final Widget? childWhenDragging; - final Widget feedback; - - /// Called when the draggable starts being dragged. - final VoidCallback? onDragStarted; - - final DragUpdateCallback? onDragUpdate; - - final DraggableCanceledCallback? onDraggableCanceled; - - final VoidCallback? onDragCompleted; - final DragEndCallback? onDragEnd; - - @override - Widget build(BuildContext context) { - return UniversalPlatform.isMobile - ? LongPressDraggable( - data: data, - feedback: feedback, - childWhenDragging: childWhenDragging, - onDragUpdate: onDragUpdate, - onDragEnd: onDragEnd, - onDraggableCanceled: onDraggableCanceled, - child: child, - ) - : Draggable( - data: data, - feedback: feedback, - childWhenDragging: childWhenDragging, - onDragUpdate: onDragUpdate, - onDragEnd: onDragEnd, - onDraggableCanceled: onDraggableCanceled, - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/edit_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/edit_panel.dart index 70d6479cf5..588af73592 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/edit_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/edit_panel.dart @@ -10,14 +10,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; class EditPanel extends StatelessWidget { - const EditPanel({ - super.key, - required this.panelContext, - required this.onEndEdit, - }); - final EditPanelContext panelContext; final VoidCallback onEndEdit; + const EditPanel({ + Key? key, + required this.panelContext, + required this.onEndEdit, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -44,10 +43,8 @@ class EditPanel extends StatelessWidget { } class EditPanelTopBar extends StatelessWidget { - const EditPanelTopBar({super.key, required this.onClose}); - final VoidCallback onClose; - + const EditPanelTopBar({Key? key, required this.onClose}) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart index 1338048e1b..8bf9d8ce31 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart @@ -1,16 +1,6 @@ import 'package:flutter/material.dart'; class AnimatedPanel extends StatefulWidget { - const AnimatedPanel({ - super.key, - this.isClosed = false, - this.closedX = 0.0, - this.closedY = 0.0, - this.duration = 0.0, - this.curve, - this.child, - }); - final bool isClosed; final double closedX; final double closedY; @@ -18,6 +8,16 @@ class AnimatedPanel extends StatefulWidget { final Curve? curve; final Widget? child; + const AnimatedPanel({ + Key? key, + this.isClosed = false, + this.closedX = 0.0, + this.closedY = 0.0, + this.duration = 0.0, + this.curve, + this.child, + }) : super(key: key); + @override AnimatedPanelState createState() => AnimatedPanelState(); } @@ -40,7 +40,7 @@ class AnimatedPanelState extends State { _isHidden = widget.isClosed && value == Offset(widget.closedX, widget.closedY); return _isHidden - ? const SizedBox.shrink() + ? Container() : Transform.translate(offset: value, child: c); }, child: widget.child, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/emoji_picker.dart new file mode 100644 index 0000000000..47a8f0d3b1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/emoji_picker.dart @@ -0,0 +1,4 @@ +export 'src/config.dart'; +export 'src/models/emoji_model.dart'; +export 'src/emoji_picker.dart'; +export 'src/emoji_picker_builder.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/config.dart new file mode 100644 index 0000000000..a12d382f2c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/config.dart @@ -0,0 +1,169 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'models/category_models.dart'; +import 'emoji_picker.dart'; + +/// Config for customizations +class Config { + /// Constructor + const Config({ + this.columns = 7, + this.emojiSizeMax = 32.0, + this.verticalSpacing = 0, + this.horizontalSpacing = 0, + this.initCategory = Category.RECENT, + this.bgColor = const Color(0xFFEBEFF2), + this.indicatorColor = Colors.blue, + this.selectedHoverColor = Colors.grey, + this.iconColor = Colors.grey, + this.iconColorSelected = Colors.blue, + this.progressIndicatorColor = Colors.blue, + this.backspaceColor = Colors.blue, + this.showRecentsTab = true, + this.recentsLimit = 28, + this.noRecentsText = 'No Recents', + this.noRecentsStyle = const TextStyle(fontSize: 20, color: Colors.black26), + this.tabIndicatorAnimDuration = kTabScrollDuration, + this.categoryIcons = const CategoryIcons(), + this.buttonMode = ButtonMode.MATERIAL, + }); + + /// Number of emojis per row + final int columns; + + /// Width and height the emoji will be maximal displayed + /// Can be smaller due to screen size and amount of columns + final double emojiSizeMax; + + /// Vertical spacing between emojis + final double verticalSpacing; + + /// Horizontal spacing between emojis + final double horizontalSpacing; + + /// The initial [Category] that will be selected + /// This [Category] will have its button in the bottombar darkened + final Category initCategory; + + /// The background color of the Widget + final Color bgColor; + + /// The color of the category indicator + final Color indicatorColor; + + /// The background color of the selected category + final Color selectedHoverColor; + + /// The color of the category icons + final Color iconColor; + + /// The color of the category icon when selected + final Color iconColorSelected; + + /// The color of the loading indicator during initialization + final Color progressIndicatorColor; + + /// The color of the backspace icon button + final Color backspaceColor; + + /// Show extra tab with recently used emoji + final bool showRecentsTab; + + /// Limit of recently used emoji that will be saved + final int recentsLimit; + + /// The text to be displayed if no recent emojis to display + final String noRecentsText; + + /// The text style for [noRecentsText] + final TextStyle noRecentsStyle; + + /// Duration of tab indicator to animate to next category + final Duration tabIndicatorAnimDuration; + + /// Determines the icon to display for each [Category] + final CategoryIcons categoryIcons; + + /// Change between Material and Cupertino button style + final ButtonMode buttonMode; + + /// Get Emoji size based on properties and screen width + double getEmojiSize(double width) { + final maxSize = width / columns; + return min(maxSize, emojiSizeMax); + } + + /// Returns the icon for the category + IconData getIconForCategory(Category category) { + switch (category) { + case Category.RECENT: + return categoryIcons.recentIcon; + case Category.SMILEYS: + return categoryIcons.smileyIcon; + case Category.ANIMALS: + return categoryIcons.animalIcon; + case Category.FOODS: + return categoryIcons.foodIcon; + case Category.TRAVEL: + return categoryIcons.travelIcon; + case Category.ACTIVITIES: + return categoryIcons.activityIcon; + case Category.OBJECTS: + return categoryIcons.objectIcon; + case Category.SYMBOLS: + return categoryIcons.symbolIcon; + case Category.FLAGS: + return categoryIcons.flagIcon; + case Category.SEARCH: + return categoryIcons.searchIcon; + default: + throw Exception('Unsupported Category'); + } + } + + @override + bool operator ==(other) { + return (other is Config) && + other.columns == columns && + other.emojiSizeMax == emojiSizeMax && + other.verticalSpacing == verticalSpacing && + other.horizontalSpacing == horizontalSpacing && + other.initCategory == initCategory && + other.bgColor == bgColor && + other.indicatorColor == indicatorColor && + other.iconColor == iconColor && + other.iconColorSelected == iconColorSelected && + other.progressIndicatorColor == progressIndicatorColor && + other.backspaceColor == backspaceColor && + other.showRecentsTab == showRecentsTab && + other.recentsLimit == recentsLimit && + other.noRecentsText == noRecentsText && + other.noRecentsStyle == noRecentsStyle && + other.tabIndicatorAnimDuration == tabIndicatorAnimDuration && + other.categoryIcons == categoryIcons && + other.buttonMode == buttonMode; + } + + @override + int get hashCode => + columns.hashCode ^ + emojiSizeMax.hashCode ^ + verticalSpacing.hashCode ^ + horizontalSpacing.hashCode ^ + initCategory.hashCode ^ + bgColor.hashCode ^ + indicatorColor.hashCode ^ + iconColor.hashCode ^ + iconColorSelected.hashCode ^ + progressIndicatorColor.hashCode ^ + backspaceColor.hashCode ^ + showRecentsTab.hashCode ^ + recentsLimit.hashCode ^ + noRecentsText.hashCode ^ + noRecentsStyle.hashCode ^ + tabIndicatorAnimDuration.hashCode ^ + categoryIcons.hashCode ^ + buttonMode.hashCode; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart new file mode 100644 index 0000000000..a663f0b5cc --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart @@ -0,0 +1,321 @@ +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'models/category_models.dart'; +import 'config.dart'; +import 'models/emoji_model.dart'; +import 'emoji_picker.dart'; +import 'emoji_picker_builder.dart'; +import 'emoji_view_state.dart'; + +class DefaultEmojiPickerView extends EmojiPickerBuilder { + const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key}) + : super(config, state, key: key); + + @override + DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState(); +} + +class DefaultEmojiPickerViewState extends State + with TickerProviderStateMixin { + PageController? _pageController; + TabController? _tabController; + final TextEditingController _emojiController = TextEditingController(); + final FocusNode _emojiFocusNode = FocusNode(); + final CategoryEmoji _categoryEmoji = + CategoryEmoji(Category.SEARCH, List.empty(growable: true)); + CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, []); + + @override + void initState() { + var initCategory = widget.state.categoryEmoji.indexWhere( + (element) => element.category == widget.config.initCategory, + ); + if (initCategory == -1) { + initCategory = 0; + } + _tabController = TabController( + initialIndex: initCategory, + length: widget.state.categoryEmoji.length, + vsync: this, + ); + _pageController = PageController(initialPage: initCategory); + _emojiFocusNode.requestFocus(); + + _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.categoryEmoji) { + searchEmojiList.emoji.addAll( + element.emoji.where((item) { + return item.name.toLowerCase().contains(query); + }).toList(), + ); + } + } + setState(() {}); + }); + super.initState(); + } + + @override + void dispose() { + _emojiController.dispose(); + _emojiFocusNode.dispose(); + super.dispose(); + } + + 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!(); + }, + ), + ); + } + return Container(); + } + + bool isEmojiSearching() { + final bool result = + searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; + + return result; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final emojiSize = widget.config.getEmojiSize(constraints.maxWidth); + + return Container( + color: widget.config.bgColor, + padding: const EdgeInsets.all(4.0), + child: Column( + children: [ + SizedBox( + height: 40, + child: TextField( + controller: _emojiController, + focusNode: _emojiFocusNode, + autofocus: true, + cursorWidth: 1.0, + cursorColor: Theme.of(context).colorScheme.tertiary, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: FontSizes.s16, + fontWeight: FontWeight.w400, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 10), + hintText: "Search emoji", + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.tertiary, + width: 2, + ), + ), + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.tertiary, + ), + ), + filled: true, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + ), + ), + ), + const VSpace(6), + Row( + children: [ + Expanded( + child: TabBar( + labelColor: widget.config.iconColorSelected, + unselectedLabelColor: widget.config.iconColor, + controller: isEmojiSearching() + ? TabController(length: 1, vsync: this) + : _tabController, + labelPadding: EdgeInsets.zero, + indicatorColor: widget.config.indicatorColor, + padding: const EdgeInsets.symmetric(vertical: 5.0), + indicator: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + color: widget.config.selectedHoverColor, + ), + onTap: (index) { + _pageController!.animateToPage( + index, + duration: widget.config.tabIndicatorAnimDuration, + curve: Curves.ease, + ); + }, + tabs: isEmojiSearching() + ? [_buildCategory(Category.SEARCH, emojiSize)] + : widget.state.categoryEmoji + .asMap() + .entries + .map( + (item) => _buildCategory( + item.value.category, + emojiSize, + ), + ) + .toList(), + ), + ), + _buildBackspaceButton(), + ], + ), + Flexible( + child: PageView.builder( + itemCount: searchEmojiList.emoji.isNotEmpty + ? 1 + : widget.state.categoryEmoji.length, + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + // onPageChanged: (index) { + // _tabController!.animateTo( + // index, + // duration: widget.config.tabIndicatorAnimDuration, + // ); + // }, + itemBuilder: (context, index) { + final CategoryEmoji catEmoji = isEmojiSearching() + ? searchEmojiList + : widget.state.categoryEmoji[index]; + return _buildPage(emojiSize, catEmoji); + }, + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildCategory(Category category, double categorySize) { + return Tab( + height: categorySize, + child: Icon( + widget.config.getIconForCategory(category), + size: categorySize / 1.3, + ), + ); + } + + Widget _buildButtonWidget({ + required VoidCallback onPressed, + required Widget child, + }) { + if (widget.config.buttonMode == ButtonMode.MATERIAL) { + return TextButton( + onPressed: onPressed, + style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), + child: child, + ); + } + return CupertinoButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + child: child, + ); + } + + Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) { + // Display notice if recent has no entries yet + final scrollController = ScrollController(); + + if (categoryEmoji.category == Category.RECENT && + categoryEmoji.emoji.isEmpty) { + return _buildNoRecent(); + } else if (categoryEmoji.category == Category.SEARCH && + categoryEmoji.emoji.isEmpty) { + return const Center(child: Text("No Emoji Found")); + } + // Build page normally + return ScrollbarListStack( + axis: Axis.vertical, + controller: scrollController, + barSize: 4.0, + scrollbarPadding: const EdgeInsets.symmetric(horizontal: 5.0), + handleColor: const Color(0xffDFE0E0), + trackColor: const Color(0xffDFE0E0), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: GridView.count( + scrollDirection: Axis.vertical, + physics: const ScrollPhysics(), + controller: scrollController, + shrinkWrap: true, + // primary: true, + padding: const EdgeInsets.all(0), + crossAxisCount: widget.config.columns, + mainAxisSpacing: widget.config.verticalSpacing, + crossAxisSpacing: widget.config.horizontalSpacing, + children: _categoryEmoji.emoji.isNotEmpty + ? _categoryEmoji.emoji + .map((e) => _buildEmoji(emojiSize, categoryEmoji, e)) + .toList() + : categoryEmoji.emoji + .map( + (item) => _buildEmoji(emojiSize, categoryEmoji, item), + ) + .toList(), + ), + ), + ); + } + + Widget _buildEmoji( + double emojiSize, + CategoryEmoji categoryEmoji, + Emoji emoji, + ) { + return _buildButtonWidget( + onPressed: () { + widget.state.onEmojiSelected(categoryEmoji.category, emoji); + }, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + emoji.emoji, + textScaleFactor: 1.0, + style: TextStyle( + fontSize: emojiSize, + backgroundColor: Colors.transparent, + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ); + } + + Widget _buildNoRecent() { + return Center( + child: Text( + widget.config.noRecentsText, + style: widget.config.noRecentsStyle, + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart new file mode 100644 index 0000000000..6cb3681206 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart @@ -0,0 +1,3223 @@ +// Copyright information +// File originally from https://github.com/JeffG05/emoji_picker + +// import 'emoji.dart'; + +// final List> temp = [smileys, animals, foods, activities, travel, objects, symbols, flags]; + +// final List emojiSearchList = temp +// .map((element) { +// return element.entries.map((entry) => Emoji(entry.key, entry.value)).toList(); +// }) +// .toList() +// .first; + +/// Map of all possible emojis along with their names in [Category.SMILEYS] +final Map smileys = Map.fromIterables([ + 'Grinning Face', + 'Grinning Face With Big Eyes', + 'Grinning Face With Smiling Eyes', + 'Beaming Face With Smiling Eyes', + 'Grinning Squinting Face', + 'Grinning Face With Sweat', + 'Rolling on the Floor Laughing', + 'Face With Tears of Joy', + 'Slightly Smiling Face', + 'Upside-Down Face', + 'Winking Face', + 'Smiling Face With Smiling Eyes', + 'Smiling Face With Halo', + 'Smiling Face With Hearts', + 'Smiling Face With Heart-Eyes', + 'Star-Struck', + 'Face Blowing a Kiss', + 'Kissing Face', + 'Smiling Face', + 'Kissing Face With Closed Eyes', + 'Kissing Face With Smiling Eyes', + 'Face Savoring Food', + 'Face With Tongue', + 'Winking Face With Tongue', + 'Zany Face', + 'Squinting Face With Tongue', + 'Money-Mouth Face', + 'Hugging Face', + 'Face With Hand Over Mouth', + 'Shushing Face', + 'Thinking Face', + 'Zipper-Mouth Face', + 'Face With Raised Eyebrow', + 'Neutral Face', + 'Expressionless Face', + 'Face Without Mouth', + 'Smirking Face', + 'Unamused Face', + 'Face With Rolling Eyes', + 'Grimacing Face', + 'Lying Face', + 'Relieved Face', + 'Pensive Face', + 'Sleepy Face', + 'Drooling Face', + 'Sleeping Face', + 'Face With Medical Mask', + 'Face With Thermometer', + 'Face With Head-Bandage', + 'Nauseated Face', + 'Face Vomiting', + 'Sneezing Face', + 'Hot Face', + 'Cold Face', + 'Woozy Face', + 'Dizzy Face', + 'Exploding Head', + 'Cowboy Hat Face', + 'Partying Face', + 'Smiling Face With Sunglasses', + 'Nerd Face', + 'Face With Monocle', + 'Confused Face', + 'Worried Face', + 'Slightly Frowning Face', + 'Frowning Face', + 'Face With Open Mouth', + 'Hushed Face', + 'Astonished Face', + 'Flushed Face', + 'Pleading Face', + 'Frowning Face With Open Mouth', + 'Anguished Face', + 'Fearful Face', + 'Anxious Face With Sweat', + 'Sad but Relieved Face', + 'Crying Face', + 'Loudly Crying Face', + 'Face Screaming in Fear', + 'Confounded Face', + 'Persevering Face', + 'Disappointed Face', + 'Downcast Face With Sweat', + 'Weary Face', + 'Tired Face', + 'Face With Steam From Nose', + 'Pouting Face', + 'Angry Face', + 'Face With Symbols on Mouth', + 'Smiling Face With Horns', + 'Angry Face With Horns', + 'Skull', + 'Skull and Crossbones', + 'Pile of Poo', + 'Clown Face', + 'Ogre', + 'Goblin', + 'Ghost', + 'Alien', + 'Alien Monster', + 'Robot Face', + 'Grinning Cat Face', + 'Grinning Cat Face With Smiling Eyes', + 'Cat Face With Tears of Joy', + 'Smiling Cat Face With Heart-Eyes', + 'Cat Face With Wry Smile', + 'Kissing Cat Face', + 'Weary Cat Face', + 'Crying Cat Face', + 'Pouting Cat Face', + 'Kiss Mark', + 'Waving Hand', + 'Raised Back of Hand', + 'Hand With Fingers Splayed', + 'Raised Hand', + 'Vulcan Salute', + 'OK Hand', + 'Victory Hand', + 'Crossed Fingers', + 'Love-You Gesture', + 'Sign of the Horns', + 'Call Me Hand', + 'Backhand Index Pointing Left', + 'Backhand Index Pointing Right', + 'Backhand Index Pointing Up', + 'Middle Finger', + 'Backhand Index Pointing Down', + 'Index Pointing Up', + 'Thumbs Up', + 'Thumbs Down', + 'Raised Fist', + 'Oncoming Fist', + 'Left-Facing Fist', + 'Right-Facing Fist', + 'Clapping Hands', + 'Raising Hands', + 'Open Hands', + 'Palms Up Together', + 'Handshake', + 'Folded Hands', + 'Writing Hand', + 'Nail Polish', + 'Selfie', + 'Flexed Biceps', + 'Leg', + 'Foot', + 'Ear', + 'Nose', + 'Brain', + 'Tooth', + 'Bone', + 'Eyes', + 'Eye', + 'Tongue', + 'Mouth', + 'Baby', + 'Child', + 'Boy', + 'Girl', + 'Person', + 'Man', + 'Man: Beard', + 'Man: Blond Hair', + 'Man: Red Hair', + 'Man: Curly Hair', + 'Man: White Hair', + 'Man: Bald', + 'Woman', + 'Woman: Blond Hair', + 'Woman: Red Hair', + 'Woman: Curly Hair', + 'Woman: White Hair', + 'Woman: Bald', + 'Older Person', + 'Old Man', + 'Old Woman', + 'Man Frowning', + 'Woman Frowning', + 'Man Pouting', + 'Woman Pouting', + 'Man Gesturing No', + 'Woman Gesturing No', + 'Man Gesturing OK', + 'Woman Gesturing OK', + 'Man Tipping Hand', + 'Woman Tipping Hand', + 'Man Raising Hand', + 'Woman Raising Hand', + 'Man Bowing', + 'Woman Bowing', + 'Man Facepalming', + 'Woman Facepalming', + 'Man Shrugging', + 'Woman Shrugging', + 'Man Health Worker', + 'Woman Health Worker', + 'Man Student', + 'Woman Student', + 'Man Teacher', + 'Woman Teacher', + 'Man Judge', + 'Woman Judge', + 'Man Farmer', + 'Woman Farmer', + 'Man Cook', + 'Woman Cook', + 'Man Mechanic', + 'Woman Mechanic', + 'Man Factory Worker', + 'Woman Factory Worker', + 'Man Office Worker', + 'Woman Office Worker', + 'Man Scientist', + 'Woman Scientist', + 'Man Technologist', + 'Woman Technologist', + 'Man Singer', + 'Woman Singer', + 'Man Artist', + 'Woman Artist', + 'Man Pilot', + 'Woman Pilot', + 'Man Astronaut', + 'Woman Astronaut', + 'Man Firefighter', + 'Woman Firefighter', + 'Man Police Officer', + 'Woman Police Officer', + 'Man Detective', + 'Woman Detective', + 'Man Guard', + 'Woman Guard', + 'Man Construction Worker', + 'Woman Construction Worker', + 'Prince', + 'Princess', + 'Man Wearing Turban', + 'Woman Wearing Turban', + 'Man With Chinese Cap', + 'Woman With Headscarf', + 'Man in Tuxedo', + 'Bride With Veil', + 'Pregnant Woman', + 'Breast-Feeding', + 'Baby Angel', + 'Santa Claus', + 'Mrs. Claus', + 'Man Superhero', + 'Woman Superhero', + 'Man Supervillain', + 'Woman Supervillain', + 'Man Mage', + 'Woman Mage', + 'Man Fairy', + 'Woman Fairy', + 'Man Vampire', + 'Woman Vampire', + 'Merman', + 'Mermaid', + 'Man Elf', + 'Woman Elf', + 'Man Genie', + 'Woman Genie', + 'Man Zombie', + 'Woman Zombie', + 'Man Getting Massage', + 'Woman Getting Massage', + 'Man Getting Haircut', + 'Woman Getting Haircut', + 'Man Walking', + 'Woman Walking', + 'Man Running', + 'Woman Running', + 'Woman Dancing', + 'Man Dancing', + 'Man in Suit Levitating', + 'Men With Bunny Ears', + 'Women With Bunny Ears', + 'Man in Steamy Room', + 'Woman in Steamy Room', + 'Person in Lotus Position', + 'Women Holding Hands', + 'Woman and Man Holding Hands', + 'Men Holding Hands', + 'Kiss', + 'Kiss: Man, Man', + 'Kiss: Woman, Woman', + 'Couple With Heart', + 'Couple With Heart: Man, Man', + 'Couple With Heart: Woman, Woman', + 'Family', + 'Family: Man, Woman, Boy', + 'Family: Man, Woman, Girl', + 'Family: Man, Woman, Girl, Boy', + 'Family: Man, Woman, Boy, Boy', + 'Family: Man, Woman, Girl, Girl', + 'Family: Man, Man, Boy', + 'Family: Man, Man, Girl', + 'Family: Man, Man, Girl, Boy', + 'Family: Man, Man, Boy, Boy', + 'Family: Man, Man, Girl, Girl', + 'Family: Woman, Woman, Boy', + 'Family: Woman, Woman, Girl', + 'Family: Woman, Woman, Girl, Boy', + 'Family: Woman, Woman, Boy, Boy', + 'Family: Woman, Woman, Girl, Girl', + 'Family: Man, Boy', + 'Family: Man, Boy, Boy', + 'Family: Man, Girl', + 'Family: Man, Girl, Boy', + 'Family: Man, Girl, Girl', + 'Family: Woman, Boy', + 'Family: Woman, Boy, Boy', + 'Family: Woman, Girl', + 'Family: Woman, Girl, Boy', + 'Family: Woman, Girl, Girl', + 'Speaking Head', + 'Bust in Silhouette', + 'Busts in Silhouette', + 'Footprints', + 'Luggage', + 'Closed Umbrella', + 'Umbrella', + 'Thread', + 'Yarn', + 'Glasses', + 'Sunglasses', + 'Goggles', + 'Lab Coat', + 'Necktie', + 'T-Shirt', + 'Jeans', + 'Scarf', + 'Gloves', + 'Coat', + 'Socks', + 'Dress', + 'Kimono', + 'Bikini', + 'Woman’s Clothes', + 'Purse', + 'Handbag', + 'Clutch Bag', + 'Backpack', + 'Man’s Shoe', + 'Running Shoe', + 'Hiking Boot', + 'Flat Shoe', + 'High-Heeled Shoe', + 'Woman’s Sandal', + 'Woman’s Boot', + 'Crown', + 'Woman’s Hat', + 'Top Hat', + 'Graduation Cap', + 'Billed Cap', + 'Rescue Worker’s Helmet', + 'Lipstick', + 'Ring', + 'Briefcase' +], [ + '😀', + '😃', + '😄', + '😁', + '😆', + '😅', + '🤣', + '😂', + '🙂', + '🙃', + '😉', + '😊', + '😇', + '🥰', + '😍', + '🤩', + '😘', + '😗', + '☺', + '😚', + '😙', + '😋', + '😛', + '😜', + '🤪', + '😝', + '🤑', + '🤗', + '🤭', + '🤫', + '🤔', + '🤐', + '🤨', + '😐', + '😑', + '😶', + '😏', + '😒', + '🙄', + '😬', + '🤥', + '😌', + '😔', + '😪', + '🤤', + '😴', + '😷', + '🤒', + '🤕', + '🤢', + '🤮', + '🤧', + '🥵', + '🥶', + '🥴', + '😵', + '🤯', + '🤠', + '🥳', + '😎', + '🤓', + '🧐', + '😕', + '😟', + '🙁', + '☹️', + '😮', + '😯', + '😲', + '😳', + '🥺', + '😦', + '😧', + '😨', + '😰', + '😥', + '😢', + '😭', + '😱', + '😖', + '😣', + '😞', + '😓', + '😩', + '😫', + '😤', + '😡', + '😠', + '🤬', + '😈', + '👿', + '💀', + '☠', + '💩', + '🤡', + '👹', + '👺', + '👻', + '👽', + '👾', + '🤖', + '😺', + '😸', + '😹', + '😻', + '😼', + '😽', + '🙀', + '😿', + '😾', + '💋', + '👋', + '🤚', + '🖐', + '✋', + '🖖', + '👌', + '✌', + '🤞', + '🤟', + '🤘', + '🤙', + '👈', + '👉', + '👆', + '🖕', + '👇', + '☝', + '👍', + '👎', + '✊', + '👊', + '🤛', + '🤜', + '👏', + '🙌', + '👐', + '🤲', + '🤝', + '🙏', + '✍', + '💅', + '🤳', + '💪', + '🦵', + '🦶', + '👂', + '👃', + '🧠', + '🦷', + '🦴', + '👀', + '👁', + '👅', + '👄', + '👶', + '🧒', + '👦', + '👧', + '🧑', + '👨', + '🧔', + '👱', + '👨‍🦰', + '👨‍🦱', + '👨‍🦳', + '👨‍🦲', + '👩', + '👱', + '👩‍🦰', + '👩‍🦱', + '👩‍🦳', + '👩‍🦲', + '🧓', + '👴', + '👵', + '🙍', + '🙍', + '🙎', + '🙎', + '🙅', + '🙅', + '🙆', + '🙆', + '💁', + '💁', + '🙋', + '🙋', + '🙇', + '🙇', + '🤦', + '🤦', + '🤷', + '🤷', + '👨‍⚕️', + '👩‍⚕️', + '👨‍🎓', + '👩‍🎓', + '👨‍🏫', + '👩‍🏫', + '👨‍⚖️', + '👩‍⚖️', + '👨‍🌾', + '👩‍🌾', + '👨‍🍳', + '👩‍🍳', + '👨‍🔧', + '👩‍🔧', + '👨‍🏭', + '👩‍🏭', + '👨‍💼', + '👩‍💼', + '👨‍🔬', + '👩‍🔬', + '👨‍💻', + '👩‍💻', + '👨‍🎤', + '👩‍🎤', + '👨‍🎨', + '👩‍🎨', + '👨‍✈️', + '👩‍✈️', + '👨‍🚀', + '👩‍🚀', + '👨‍🚒', + '👩‍🚒', + '👮', + '👮', + '🕵️', + '🕵️', + '💂', + '💂', + '👷', + '👷', + '🤴', + '👸', + '👳', + '👳', + '👲', + '🧕', + '🤵', + '👰', + '🤰', + '🤱', + '👼', + '🎅', + '🤶', + '🦸', + '🦸', + '🦹', + '🦹', + '🧙', + '🧙', + '🧚', + '🧚', + '🧛', + '🧛', + '🧜', + '🧜', + '🧝', + '🧝', + '🧞', + '🧞', + '🧟', + '🧟', + '💆', + '💆', + '💇', + '💇', + '🚶', + '🚶', + '🏃', + '🏃', + '💃', + '🕺', + '🕴', + '👯', + '👯', + '🧖', + '🧖', + '🧘', + '👭', + '👫', + '👬', + '💏', + '👨‍❤️‍💋‍👨', + '👩‍❤️‍💋‍👩', + '💑', + '👨‍❤️‍👨', + '👩‍❤️‍👩', + '👪', + '👨‍👩‍👦', + '👨‍👩‍👧', + '👨‍👩‍👧‍👦', + '👨‍👩‍👦‍👦', + '👨‍👩‍👧‍👧', + '👨‍👨‍👦', + '👨‍👨‍👧', + '👨‍👨‍👧‍👦', + '👨‍👨‍👦‍👦', + '👨‍👨‍👧‍👧', + '👩‍👩‍👦', + '👩‍👩‍👧', + '👩‍👩‍👧‍👦', + '👩‍👩‍👦‍👦', + '👩‍👩‍👧‍👧', + '👨‍👦', + '👨‍👦‍👦', + '👨‍👧', + '👨‍👧‍👦', + '👨‍👧‍👧', + '👩‍👦', + '👩‍👦‍👦', + '👩‍👧', + '👩‍👧‍👦', + '👩‍👧‍👧', + '🗣', + '👤', + '👥', + '👣', + '🧳', + '🌂', + '☂', + '🧵', + '🧶', + '👓', + '🕶', + '🥽', + '🥼', + '👔', + '👕', + '👖', + '🧣', + '🧤', + '🧥', + '🧦', + '👗', + '👘', + '👙', + '👚', + '👛', + '👜', + '👝', + '🎒', + '👞', + '👟', + '🥾', + '🥿', + '👠', + '👡', + '👢', + '👑', + '👒', + '🎩', + '🎓', + '🧢', + '⛑', + '💄', + '💍', + '💼' +]); + +/// Map of all possible emojis along with their names in [Category.ANIMALS] +final Map animals = Map.fromIterables([ + 'Dog Face', + 'Cat Face', + 'Mouse Face', + 'Hamster Face', + 'Rabbit Face', + 'Fox Face', + 'Bear Face', + 'Panda Face', + 'Koala Face', + 'Tiger Face', + 'Lion Face', + 'Cow Face', + 'Pig Face', + 'Pig Nose', + 'Frog Face', + 'Monkey Face', + 'See-No-Evil Monkey', + 'Hear-No-Evil Monkey', + 'Speak-No-Evil Monkey', + 'Monkey', + 'Collision', + 'Dizzy', + 'Sweat Droplets', + 'Dashing Away', + 'Gorilla', + 'Dog', + 'Poodle', + 'Wolf Face', + 'Raccoon', + 'Cat', + 'Tiger', + 'Leopard', + 'Horse Face', + 'Horse', + 'Unicorn Face', + 'Zebra', + 'Ox', + 'Water Buffalo', + 'Cow', + 'Pig', + 'Boar', + 'Ram', + 'Ewe', + 'Goat', + 'Camel', + 'Two-Hump Camel', + 'Llama', + 'Giraffe', + 'Elephant', + 'Rhinoceros', + 'Hippopotamus', + 'Mouse', + 'Rat', + 'Rabbit', + 'Chipmunk', + 'Hedgehog', + 'Bat', + 'Kangaroo', + 'Badger', + 'Paw Prints', + 'Turkey', + 'Chicken', + 'Rooster', + 'Hatching Chick', + 'Baby Chick', + 'Front-Facing Baby Chick', + 'Bird', + 'Penguin', + 'Dove', + 'Eagle', + 'Duck', + 'Swan', + 'Owl', + 'Peacock', + 'Parrot', + 'Crocodile', + 'Turtle', + 'Lizard', + 'Snake', + 'Dragon Face', + 'Dragon', + 'Sauropod', + 'T-Rex', + 'Spouting Whale', + 'Whale', + 'Dolphin', + 'Fish', + 'Tropical Fish', + 'Blowfish', + 'Shark', + 'Octopus', + 'Spiral Shell', + 'Snail', + 'Butterfly', + 'Bug', + 'Ant', + 'Honeybee', + 'Lady Beetle', + 'Cricket', + 'Spider', + 'Spider Web', + 'Scorpion', + 'Mosquito', + 'Microbe', + 'Bouquet', + 'Cherry Blossom', + 'White Flower', + 'Rosette', + 'Rose', + 'Wilted Flower', + 'Hibiscus', + 'Sunflower', + 'Blossom', + 'Tulip', + 'Seedling', + 'Evergreen Tree', + 'Deciduous Tree', + 'Palm Tree', + 'Cactus', + 'Sheaf of Rice', + 'Herb', + 'Shamrock', + 'Four Leaf Clover', + 'Maple Leaf', + 'Fallen Leaf', + 'Leaf Fluttering in Wind', + 'Mushroom', + 'Chestnut', + 'Crab', + 'Lobster', + 'Shrimp', + 'Squid', + 'Globe Showing Europe-Africa', + 'Globe Showing Americas', + 'Globe Showing Asia-Australia', + 'Globe With Meridians', + 'New Moon', + 'Waxing Crescent Moon', + 'First Quarter Moon', + 'Waxing Gibbous Moon', + 'Full Moon', + 'Waning Gibbous Moon', + 'Last Quarter Moon', + 'Waning Crescent Moon', + 'Crescent Moon', + 'New Moon Face', + 'First Quarter Moon Face', + 'Last Quarter Moon Face', + 'Sun', + 'Full Moon Face', + 'Sun With Face', + 'Star', + 'Glowing Star', + 'Shooting Star', + 'Cloud', + 'Sun Behind Cloud', + 'Cloud With Lightning and Rain', + 'Sun Behind Small Cloud', + 'Sun Behind Large Cloud', + 'Sun Behind Rain Cloud', + 'Cloud With Rain', + 'Cloud With Snow', + 'Cloud With Lightning', + 'Tornado', + 'Fog', + 'Wind Face', + 'Rainbow', + 'Umbrella', + 'Umbrella With Rain Drops', + 'High Voltage', + 'Snowflake', + 'Snowman Without Snow', + 'Snowman', + 'Comet', + 'Fire', + 'Droplet', + 'Water Wave', + 'Christmas Tree', + 'Sparkles', + 'Tanabata Tree', + 'Pine Decoration' +], [ + '🐶', + '🐱', + '🐭', + '🐹', + '🐰', + '🦊', + '🐻', + '🐼', + '🐨', + '🐯', + '🦁', + '🐮', + '🐷', + '🐽', + '🐸', + '🐵', + '🙈', + '🙉', + '🙊', + '🐒', + '💥', + '💫', + '💦', + '💨', + '🦍', + '🐕', + '🐩', + '🐺', + '🦝', + '🐈', + '🐅', + '🐆', + '🐴', + '🐎', + '🦄', + '🦓', + '🐂', + '🐃', + '🐄', + '🐖', + '🐗', + '🐏', + '🐑', + '🐐', + '🐪', + '🐫', + '🦙', + '🦒', + '🐘', + '🦏', + '🦛', + '🐁', + '🐀', + '🐇', + '🐿', + '🦔', + '🦇', + '🦘', + '🦡', + '🐾', + '🦃', + '🐔', + '🐓', + '🐣', + '🐤', + '🐥', + '🐦', + '🐧', + '🕊', + '🦅', + '🦆', + '🦢', + '🦉', + '🦚', + '🦜', + '🐊', + '🐢', + '🦎', + '🐍', + '🐲', + '🐉', + '🦕', + '🦖', + '🐳', + '🐋', + '🐬', + '🐟', + '🐠', + '🐡', + '🦈', + '🐙', + '🐚', + '🐌', + '🦋', + '🐛', + '🐜', + '🐝', + '🐞', + '🦗', + '🕷', + '🕸', + '🦂', + '🦟', + '🦠', + '💐', + '🌸', + '💮', + '🏵', + '🌹', + '🥀', + '🌺', + '🌻', + '🌼', + '🌷', + '🌱', + '🌲', + '🌳', + '🌴', + '🌵', + '🌾', + '🌿', + '☘', + '🍀', + '🍁', + '🍂', + '🍃', + '🍄', + '🌰', + '🦀', + '🦞', + '🦐', + '🦑', + '🌍', + '🌎', + '🌏', + '🌐', + '🌑', + '🌒', + '🌓', + '🌔', + '🌕', + '🌖', + '🌗', + '🌘', + '🌙', + '🌚', + '🌛', + '🌜', + '☀', + '🌝', + '🌞', + '⭐', + '🌟', + '🌠', + '☁', + '⛅', + '⛈', + '🌤', + '🌥', + '🌦', + '🌧', + '🌨', + '🌩', + '🌪', + '🌫', + '🌬', + '🌈', + '☂', + '☔', + '⚡', + '❄', + '☃', + '⛄', + '☄', + '🔥', + '💧', + '🌊', + '🎄', + '✨', + '🎋', + '🎍' +]); + +/// Map of all possible emojis along with their names in [Category.FOODS] +final Map foods = Map.fromIterables([ + 'Grapes', + 'Melon', + 'Watermelon', + 'Tangerine', + 'Lemon', + 'Banana', + 'Pineapple', + 'Mango', + 'Red Apple', + 'Green Apple', + 'Pear', + 'Peach', + 'Cherries', + 'Strawberry', + 'Kiwi Fruit', + 'Tomato', + 'Coconut', + 'Avocado', + 'Eggplant', + 'Potato', + 'Carrot', + 'Ear of Corn', + 'Hot Pepper', + 'Cucumber', + 'Leafy Green', + 'Broccoli', + 'Mushroom', + 'Peanuts', + 'Chestnut', + 'Bread', + 'Croissant', + 'Baguette Bread', + 'Pretzel', + 'Bagel', + 'Pancakes', + 'Cheese Wedge', + 'Meat on Bone', + 'Poultry Leg', + 'Cut of Meat', + 'Bacon', + 'Hamburger', + 'French Fries', + 'Pizza', + 'Hot Dog', + 'Sandwich', + 'Taco', + 'Burrito', + 'Stuffed Flatbread', + 'Cooking', + 'Shallow Pan of Food', + 'Pot of Food', + 'Bowl With Spoon', + 'Green Salad', + 'Popcorn', + 'Salt', + 'Canned Food', + 'Bento Box', + 'Rice Cracker', + 'Rice Ball', + 'Cooked Rice', + 'Curry Rice', + 'Steaming Bowl', + 'Spaghetti', + 'Roasted Sweet Potato', + 'Oden', + 'Sushi', + 'Fried Shrimp', + 'Fish Cake With Swirl', + 'Moon Cake', + 'Dango', + 'Dumpling', + 'Fortune Cookie', + 'Takeout Box', + 'Soft Ice Cream', + 'Shaved Ice', + 'Ice Cream', + 'Doughnut', + 'Cookie', + 'Birthday Cake', + 'Shortcake', + 'Cupcake', + 'Pie', + 'Chocolate Bar', + 'Candy', + 'Lollipop', + 'Custard', + 'Honey Pot', + 'Baby Bottle', + 'Glass of Milk', + 'Hot Beverage', + 'Teacup Without Handle', + 'Sake', + 'Bottle With Popping Cork', + 'Wine Glass', + 'Cocktail Glass', + 'Tropical Drink', + 'Beer Mug', + 'Clinking Beer Mugs', + 'Clinking Glasses', + 'Tumbler Glass', + 'Cup With Straw', + 'Chopsticks', + 'Fork and Knife With Plate', + 'Fork and Knife', + 'Spoon' +], [ + '🍇', + '🍈', + '🍉', + '🍊', + '🍋', + '🍌', + '🍍', + '🥭', + '🍎', + '🍏', + '🍐', + '🍑', + '🍒', + '🍓', + '🥝', + '🍅', + '🥥', + '🥑', + '🍆', + '🥔', + '🥕', + '🌽', + '🌶', + '🥒', + '🥬', + '🥦', + '🍄', + '🥜', + '🌰', + '🍞', + '🥐', + '🥖', + '🥨', + '🥯', + '🥞', + '🧀', + '🍖', + '🍗', + '🥩', + '🥓', + '🍔', + '🍟', + '🍕', + '🌭', + '🥪', + '🌮', + '🌯', + '🥙', + '🍳', + '🥘', + '🍲', + '🥣', + '🥗', + '🍿', + '🧂', + '🥫', + '🍱', + '🍘', + '🍙', + '🍚', + '🍛', + '🍜', + '🍝', + '🍠', + '🍢', + '🍣', + '🍤', + '🍥', + '🥮', + '🍡', + '🥟', + '🥠', + '🥡', + '🍦', + '🍧', + '🍨', + '🍩', + '🍪', + '🎂', + '🍰', + '🧁', + '🥧', + '🍫', + '🍬', + '🍭', + '🍮', + '🍯', + '🍼', + '🥛', + '☕', + '🍵', + '🍶', + '🍾', + '🍷', + '🍸', + '🍹', + '🍺', + '🍻', + '🥂', + '🥃', + '🥤', + '🥢', + '🍽', + '🍴', + '🥄' +]); + +/// Map of all possible emojis along with their names in [Category.TRAVEL] +final Map travel = Map.fromIterables([ + 'Person Rowing Boat', + 'Map of Japan', + 'Snow-Capped Mountain', + 'Mountain', + 'Volcano', + 'Mount Fuji', + 'Camping', + 'Beach With Umbrella', + 'Desert', + 'Desert Island', + 'National Park', + 'Stadium', + 'Classical Building', + 'Building Construction', + 'Houses', + 'Derelict House', + 'House', + 'House With Garden', + 'Office Building', + 'Japanese Post Office', + 'Post Office', + 'Hospital', + 'Bank', + 'Hotel', + 'Love Hotel', + 'Convenience Store', + 'School', + 'Department Store', + 'Factory', + 'Japanese Castle', + 'Castle', + 'Wedding', + 'Tokyo Tower', + 'Statue of Liberty', + 'Church', + 'Mosque', + 'Synagogue', + 'Shinto Shrine', + 'Kaaba', + 'Fountain', + 'Tent', + 'Foggy', + 'Night With Stars', + 'Cityscape', + 'Sunrise Over Mountains', + 'Sunrise', + 'Cityscape at Dusk', + 'Sunset', + 'Bridge at Night', + 'Carousel Horse', + 'Ferris Wheel', + 'Roller Coaster', + 'Locomotive', + 'Railway Car', + 'High-Speed Train', + 'Bullet Train', + 'Train', + 'Metro', + 'Light Rail', + 'Station', + 'Tram', + 'Monorail', + 'Mountain Railway', + 'Tram Car', + 'Bus', + 'Oncoming Bus', + 'Trolleybus', + 'Minibus', + 'Ambulance', + 'Fire Engine', + 'Police Car', + 'Oncoming Police Car', + 'Taxi', + 'Oncoming Taxi', + 'Automobile', + 'Oncoming Automobile', + 'Delivery Truck', + 'Articulated Lorry', + 'Tractor', + 'Racing Car', + 'Motorcycle', + 'Motor Scooter', + 'Bicycle', + 'Kick Scooter', + 'Bus Stop', + 'Railway Track', + 'Fuel Pump', + 'Police Car Light', + 'Horizontal Traffic Light', + 'Vertical Traffic Light', + 'Construction', + 'Anchor', + 'Sailboat', + 'Speedboat', + 'Passenger Ship', + 'Ferry', + 'Motor Boat', + 'Ship', + 'Airplane', + 'Small Airplane', + 'Airplane Departure', + 'Airplane Arrival', + 'Seat', + 'Helicopter', + 'Suspension Railway', + 'Mountain Cableway', + 'Aerial Tramway', + 'Satellite', + 'Rocket', + 'Flying Saucer', + 'Shooting Star', + 'Milky Way', + 'Umbrella on Ground', + 'Fireworks', + 'Sparkler', + 'Moon Viewing Ceremony', + 'Yen Banknote', + 'Dollar Banknote', + 'Euro Banknote', + 'Pound Banknote', + 'Moai', + 'Passport Control', + 'Customs', + 'Baggage Claim', + 'Left Luggage' +], [ + '🚣', + '🗾', + '🏔', + '⛰', + '🌋', + '🗻', + '🏕', + '🏖', + '🏜', + '🏝', + '🏞', + '🏟', + '🏛', + '🏗', + '🏘', + '🏚', + '🏠', + '🏡', + '🏢', + '🏣', + '🏤', + '🏥', + '🏦', + '🏨', + '🏩', + '🏪', + '🏫', + '🏬', + '🏭', + '🏯', + '🏰', + '💒', + '🗼', + '🗽', + '⛪', + '🕌', + '🕍', + '⛩', + '🕋', + '⛲', + '⛺', + '🌁', + '🌃', + '🏙', + '🌄', + '🌅', + '🌆', + '🌇', + '🌉', + '🎠', + '🎡', + '🎢', + '🚂', + '🚃', + '🚄', + '🚅', + '🚆', + '🚇', + '🚈', + '🚉', + '🚊', + '🚝', + '🚞', + '🚋', + '🚌', + '🚍', + '🚎', + '🚐', + '🚑', + '🚒', + '🚓', + '🚔', + '🚕', + '🚖', + '🚗', + '🚘', + '🚚', + '🚛', + '🚜', + '🏎', + '🏍', + '🛵', + '🚲', + '🛴', + '🚏', + '🛤', + '⛽', + '🚨', + '🚥', + '🚦', + '🚧', + '⚓', + '⛵', + '🚤', + '🛳', + '⛴', + '🛥', + '🚢', + '✈', + '🛩', + '🛫', + '🛬', + '💺', + '🚁', + '🚟', + '🚠', + '🚡', + '🛰', + '🚀', + '🛸', + '🌠', + '🌌', + '⛱', + '🎆', + '🎇', + '🎑', + '💴', + '💵', + '💶', + '💷', + '🗿', + '🛂', + '🛃', + '🛄', + '🛅' +]); + +/// Map of all possible emojis along with their names in [Category.ACTIVITIES] +final Map activities = Map.fromIterables([ + 'Man in Suit Levitating', + 'Man Climbing', + 'Woman Climbing', + 'Horse Racing', + 'Skier', + 'Snowboarder', + 'Man Golfing', + 'Woman Golfing', + 'Man Surfing', + 'Woman Surfing', + 'Man Rowing Boat', + 'Woman Rowing Boat', + 'Man Swimming', + 'Woman Swimming', + 'Man Bouncing Ball', + 'Woman Bouncing Ball', + 'Man Lifting Weights', + 'Woman Lifting Weights', + 'Man Biking', + 'Woman Biking', + 'Man Mountain Biking', + 'Woman Mountain Biking', + 'Man Cartwheeling', + 'Woman Cartwheeling', + 'Men Wrestling', + 'Women Wrestling', + 'Man Playing Water Polo', + 'Woman Playing Water Polo', + 'Man Playing Handball', + 'Woman Playing Handball', + 'Man Juggling', + 'Woman Juggling', + 'Man in Lotus Position', + 'Woman in Lotus Position', + 'Circus Tent', + 'Skateboard', + 'Reminder Ribbon', + 'Admission Tickets', + 'Ticket', + 'Military Medal', + 'Trophy', + 'Sports Medal', + '1st Place Medal', + '2nd Place Medal', + '3rd Place Medal', + 'Soccer Ball', + 'Baseball', + 'Softball', + 'Basketball', + 'Volleyball', + 'American Football', + 'Rugby Football', + 'Tennis', + 'Flying Disc', + 'Bowling', + 'Cricket Game', + 'FieldPB Hockey', + 'Ice Hockey', + 'Lacrosse', + 'Ping Pong', + 'Badminton', + 'Boxing Glove', + 'Martial Arts Uniform', + 'Flag in Hole', + 'Ice Skate', + 'Fishing Pole', + 'Running Shirt', + 'Skis', + 'Sled', + 'Curling Stone', + 'Direct Hit', + 'Pool 8 Ball', + 'Video Game', + 'Slot Machine', + 'Game Die', + 'Jigsaw', + 'Chess Pawn', + 'Performing Arts', + 'Artist Palette', + 'Thread', + 'Yarn', + 'Musical Score', + 'Microphone', + 'Headphone', + 'Saxophone', + 'Guitar', + 'Musical Keyboard', + 'Trumpet', + 'Violin', + 'Drum', + 'Clapper Board', + 'Bow and Arrow' +], [ + '🕴', + '🧗', + '🧗', + '🏇', + '⛷', + '🏂', + '🏌️', + '🏌️', + '🏄', + '🏄', + '🚣', + '🚣', + '🏊', + '🏊', + '⛹️', + '⛹️', + '🏋️', + '🏋️', + '🚴', + '🚴', + '🚵', + '🚵', + '🤸', + '🤸', + '🤼', + '🤼', + '🤽', + '🤽', + '🤾', + '🤾', + '🤹', + '🤹', + '🧘🏻‍♂️', + '🧘🏻‍♀️', + '🎪', + '🛹', + '🎗', + '🎟', + '🎫', + '🎖', + '🏆', + '🏅', + '🥇', + '🥈', + '🥉', + '⚽', + '⚾', + '🥎', + '🏀', + '🏐', + '🏈', + '🏉', + '🎾', + '🥏', + '🎳', + '🏏', + '🏑', + '🏒', + '🥍', + '🏓', + '🏸', + '🥊', + '🥋', + '⛳', + '⛸', + '🎣', + '🎽', + '🎿', + '🛷', + '🥌', + '🎯', + '🎱', + '🎮', + '🎰', + '🎲', + '🧩', + '♟', + '🎭', + '🎨', + '🧵', + '🧶', + '🎼', + '🎤', + '🎧', + '🎷', + '🎸', + '🎹', + '🎺', + '🎻', + '🥁', + '🎬', + '🏹' +]); + +/// Map of all possible emojis along with their names in [Category.OBJECTS] +final Map objects = Map.fromIterables([ + 'Love Letter', + 'Hole', + 'Bomb', + 'Person Taking Bath', + 'Person in Bed', + 'Kitchen Knife', + 'Amphora', + 'World Map', + 'Compass', + 'Brick', + 'Barber Pole', + 'Oil Drum', + 'Bellhop Bell', + 'Luggage', + 'Hourglass Done', + 'Hourglass Not Done', + 'Watch', + 'Alarm Clock', + 'Stopwatch', + 'Timer Clock', + 'Mantelpiece Clock', + 'Thermometer', + 'Umbrella on Ground', + 'Firecracker', + 'Balloon', + 'Party Popper', + 'Confetti Ball', + 'Japanese Dolls', + 'Carp Streamer', + 'Wind Chime', + 'Red Envelope', + 'Ribbon', + 'Wrapped Gift', + 'Crystal Ball', + 'Nazar Amulet', + 'Joystick', + 'Teddy Bear', + 'Framed Picture', + 'Thread', + 'Yarn', + 'Shopping Bags', + 'Prayer Beads', + 'Gem Stone', + 'Postal Horn', + 'Studio Microphone', + 'Level Slider', + 'Control Knobs', + 'Radio', + 'Mobile Phone', + 'Mobile Phone With Arrow', + 'Telephone', + 'Telephone Receiver', + 'Pager', + 'Fax Machine', + 'Battery', + 'Electric Plug', + 'Laptop Computer', + 'Desktop Computer', + 'Printer', + 'Keyboard', + 'Computer Mouse', + 'Trackball', + 'Computer Disk', + 'Floppy Disk', + 'Optical Disk', + 'DVD', + 'Abacus', + 'Movie Camera', + 'Film Frames', + 'Film Projector', + 'Television', + 'Camera', + 'Camera With Flash', + 'Video Camera', + 'Videocassette', + 'Magnifying Glass Tilted Left', + 'Magnifying Glass Tilted Right', + 'Candle', + 'Light Bulb', + 'Flashlight', + 'Red Paper Lantern', + 'Notebook With Decorative Cover', + 'Closed Book', + 'Open Book', + 'Green Book', + 'Blue Book', + 'Orange Book', + 'Books', + 'Notebook', + 'Page With Curl', + 'Scroll', + 'Page Facing Up', + 'Newspaper', + 'Rolled-Up Newspaper', + 'Bookmark Tabs', + 'Bookmark', + 'Label', + 'Money Bag', + 'Yen Banknote', + 'Dollar Banknote', + 'Euro Banknote', + 'Pound Banknote', + 'Money With Wings', + 'Credit Card', + 'Receipt', + 'Envelope', + 'E-Mail', + 'Incoming Envelope', + 'Envelope With Arrow', + 'Outbox Tray', + 'Inbox Tray', + 'Package', + 'Closed Mailbox With Raised Flag', + 'Closed Mailbox With Lowered Flag', + 'Open Mailbox With Raised Flag', + 'Open Mailbox With Lowered Flag', + 'Postbox', + 'Ballot Box With Ballot', + 'Pencil', + 'Black Nib', + 'Fountain Pen', + 'Pen', + 'Paintbrush', + 'Crayon', + 'Memo', + 'File Folder', + 'Open File Folder', + 'Card Index Dividers', + 'Calendar', + 'Tear-Off Calendar', + 'Spiral Notepad', + 'Spiral Calendar', + 'Card Index', + 'Chart Increasing', + 'Chart Decreasing', + 'Bar Chart', + 'Clipboard', + 'Pushpin', + 'Round Pushpin', + 'Paperclip', + 'Linked Paperclips', + 'Straight Ruler', + 'Triangular Ruler', + 'Scissors', + 'Card File Box', + 'File Cabinet', + 'Wastebasket', + 'Locked', + 'Unlocked', + 'Locked With Pen', + 'Locked With Key', + 'Key', + 'Old Key', + 'Hammer', + 'Pick', + 'Hammer and Pick', + 'Hammer and Wrench', + 'Dagger', + 'Crossed Swords', + 'Pistol', + 'Shield', + 'Wrench', + 'Nut and Bolt', + 'Gear', + 'Clamp', + 'Balance Scale', + 'Link', + 'Chains', + 'Toolbox', + 'Magnet', + 'Alembic', + 'Test Tube', + 'Petri Dish', + 'DNA', + 'Microscope', + 'Telescope', + 'Satellite Antenna', + 'Syringe', + 'Pill', + 'Door', + 'Bed', + 'Couch and Lamp', + 'Toilet', + 'Shower', + 'Bathtub', + 'Lotion Bottle', + 'Safety Pin', + 'Broom', + 'Basket', + 'Roll of Paper', + 'Soap', + 'Sponge', + 'Fire Extinguisher', + 'Cigarette', + 'Coffin', + 'Funeral Urn', + 'Moai', + 'Potable Water' +], [ + '💌', + '🕳', + '💣', + '🛀', + '🛌', + '🔪', + '🏺', + '🗺', + '🧭', + '🧱', + '💈', + '🛢', + '🛎', + '🧳', + '⌛', + '⏳', + '⌚', + '⏰', + '⏱', + '⏲', + '🕰', + '🌡', + '⛱', + '🧨', + '🎈', + '🎉', + '🎊', + '🎎', + '🎏', + '🎐', + '🧧', + '🎀', + '🎁', + '🔮', + '🧿', + '🕹', + '🧸', + '🖼', + '🧵', + '🧶', + '🛍', + '📿', + '💎', + '📯', + '🎙', + '🎚', + '🎛', + '📻', + '📱', + '📲', + '☎', + '📞', + '📟', + '📠', + '🔋', + '🔌', + '💻', + '🖥', + '🖨', + '⌨', + '🖱', + '🖲', + '💽', + '💾', + '💿', + '📀', + '🧮', + '🎥', + '🎞', + '📽', + '📺', + '📷', + '📸', + '📹', + '📼', + '🔍', + '🔎', + '🕯', + '💡', + '🔦', + '🏮', + '📔', + '📕', + '📖', + '📗', + '📘', + '📙', + '📚', + '📓', + '📃', + '📜', + '📄', + '📰', + '🗞', + '📑', + '🔖', + '🏷', + '💰', + '💴', + '💵', + '💶', + '💷', + '💸', + '💳', + '🧾', + '✉', + '📧', + '📨', + '📩', + '📤', + '📥', + '📦', + '📫', + '📪', + '📬', + '📭', + '📮', + '🗳', + '✏', + '✒', + '🖋', + '🖊', + '🖌', + '🖍', + '📝', + '📁', + '📂', + '🗂', + '📅', + '📆', + '🗒', + '🗓', + '📇', + '📈', + '📉', + '📊', + '📋', + '📌', + '📍', + '📎', + '🖇', + '📏', + '📐', + '✂', + '🗃', + '🗄', + '🗑', + '🔒', + '🔓', + '🔏', + '🔐', + '🔑', + '🗝', + '🔨', + '⛏', + '⚒', + '🛠', + '🗡', + '⚔', + '🔫', + '🛡', + '🔧', + '🔩', + '⚙', + '🗜', + '⚖', + '🔗', + '⛓', + '🧰', + '🧲', + '⚗', + '🧪', + '🧫', + '🧬', + '🔬', + '🔭', + '📡', + '💉', + '💊', + '🚪', + '🛏', + '🛋', + '🚽', + '🚿', + '🛁', + '🧴', + '🧷', + '🧹', + '🧺', + '🧻', + '🧼', + '🧽', + '🧯', + '🚬', + '⚰', + '⚱', + '🗿', + '🚰' +]); + +/// Map of all possible emojis along with their names in [Category.SYMBOLS] +final Map symbols = Map.fromIterables([ + 'Heart With Arrow', + 'Heart With Ribbon', + 'Sparkling Heart', + 'Growing Heart', + 'Beating Heart', + 'Revolving Hearts', + 'Two Hearts', + 'Heart Decoration', + 'Heavy Heart Exclamation', + 'Broken Heart', + 'Red Heart', + 'Orange Heart', + 'Yellow Heart', + 'Green Heart', + 'Blue Heart', + 'Purple Heart', + 'Black Heart', + 'Hundred Points', + 'Anger Symbol', + 'Speech Balloon', + 'Eye in Speech Bubble', + 'Right Anger Bubble', + 'Thought Balloon', + 'Zzz', + 'White Flower', + 'Hot Springs', + 'Barber Pole', + 'Stop Sign', + 'Twelve O’Clock', + 'Twelve-Thirty', + 'One O’Clock', + 'One-Thirty', + 'Two O’Clock', + 'Two-Thirty', + 'Three O’Clock', + 'Three-Thirty', + 'Four O’Clock', + 'Four-Thirty', + 'Five O’Clock', + 'Five-Thirty', + 'Six O’Clock', + 'Six-Thirty', + 'Seven O’Clock', + 'Seven-Thirty', + 'Eight O’Clock', + 'Eight-Thirty', + 'Nine O’Clock', + 'Nine-Thirty', + 'Ten O’Clock', + 'Ten-Thirty', + 'Eleven O’Clock', + 'Eleven-Thirty', + 'Cyclone', + 'Spade Suit', + 'Heart Suit', + 'Diamond Suit', + 'Club Suit', + 'Joker', + 'Mahjong Red Dragon', + 'Flower Playing Cards', + 'Muted Speaker', + 'Speaker Low Volume', + 'Speaker Medium Volume', + 'Speaker High Volume', + 'Loudspeaker', + 'Megaphone', + 'Postal Horn', + 'Bell', + 'Bell With Slash', + 'Musical Note', + 'Musical Notes', + 'ATM Sign', + 'Litter in Bin Sign', + 'Potable Water', + 'Wheelchair Symbol', + 'Men’s Room', + 'Women’s Room', + 'Restroom', + 'Baby Symbol', + 'Water Closet', + 'Warning', + 'Children Crossing', + 'No Entry', + 'Prohibited', + 'No Bicycles', + 'No Smoking', + 'No Littering', + 'Non-Potable Water', + 'No Pedestrians', + 'No One Under Eighteen', + 'Radioactive', + 'Biohazard', + 'Up Arrow', + 'Up-Right Arrow', + 'Right Arrow', + 'Down-Right Arrow', + 'Down Arrow', + 'Down-Left Arrow', + 'Left Arrow', + 'Up-Left Arrow', + 'Up-Down Arrow', + 'Left-Right Arrow', + 'Right Arrow Curving Left', + 'Left Arrow Curving Right', + 'Right Arrow Curving Up', + 'Right Arrow Curving Down', + 'Clockwise Vertical Arrows', + 'Counterclockwise Arrows Button', + 'Back Arrow', + 'End Arrow', + 'On! Arrow', + 'Soon Arrow', + 'Top Arrow', + 'Place of Worship', + 'Atom Symbol', + 'Om', + 'Star of David', + 'Wheel of Dharma', + 'Yin Yang', + 'Latin Cross', + 'Orthodox Cross', + 'Star and Crescent', + 'Peace Symbol', + 'Menorah', + 'Dotted Six-Pointed Star', + 'Aries', + 'Taurus', + 'Gemini', + 'Cancer', + 'Leo', + 'Virgo', + 'Libra', + 'Scorpio', + 'Sagittarius', + 'Capricorn', + 'Aquarius', + 'Pisces', + 'Ophiuchus', + 'Shuffle Tracks Button', + 'Repeat Button', + 'Repeat Single Button', + 'Play Button', + 'Fast-Forward Button', + 'Reverse Button', + 'Fast Reverse Button', + 'Upwards Button', + 'Fast Up Button', + 'Downwards Button', + 'Fast Down Button', + 'Stop Button', + 'Eject Button', + 'Cinema', + 'Dim Button', + 'Bright Button', + 'Antenna Bars', + 'Vibration Mode', + 'Mobile Phone Off', + 'Infinity', + 'Recycling Symbol', + 'Trident Emblem', + 'Name Badge', + 'Japanese Symbol for Beginner', + 'Heavy Large Circle', + 'White Heavy Check Mark', + 'Ballot Box With Check', + 'Heavy Check Mark', + 'Heavy Multiplication X', + 'Cross Mark', + 'Cross Mark Button', + 'Heavy Plus Sign', + 'Heavy Minus Sign', + 'Heavy Division Sign', + 'Curly Loop', + 'Double Curly Loop', + 'Part Alternation Mark', + 'Eight-Spoked Asterisk', + 'Eight-Pointed Star', + 'Sparkle', + 'Double Exclamation Mark', + 'Exclamation Question Mark', + 'Question Mark', + 'White Question Mark', + 'White Exclamation Mark', + 'Exclamation Mark', + 'Copyright', + 'Registered', + 'Trade Mark', + 'Keycap Number Sign', + 'Keycap Digit Zero', + 'Keycap Digit One', + 'Keycap Digit Two', + 'Keycap Digit Three', + 'Keycap Digit Four', + 'Keycap Digit Five', + 'Keycap Digit Six', + 'Keycap Digit Seven', + 'Keycap Digit Eight', + 'Keycap Digit Nine', + 'Keycap: 10', + 'Input Latin Uppercase', + 'Input Latin Lowercase', + 'Input Numbers', + 'Input Symbols', + 'Input Latin Letters', + 'A Button (Blood Type)', + 'AB Button (Blood Type)', + 'B Button (Blood Type)', + 'CL Button', + 'Cool Button', + 'Free Button', + 'Information', + 'ID Button', + 'Circled M', + 'New Button', + 'NG Button', + 'O Button (Blood Type)', + 'OK Button', + 'P Button', + 'SOS Button', + 'Up! Button', + 'Vs Button', + 'Japanese “Here” Button', + 'Japanese “Service Charge” Button', + 'Japanese “Monthly Amount” Button', + 'Japanese “Not Free of Charge” Button', + 'Japanese “Reserved” Button', + 'Japanese “Bargain” Button', + 'Japanese “Discount” Button', + 'Japanese “Free of Charge” Button', + 'Japanese “Prohibited” Button', + 'Japanese “Acceptable” Button', + 'Japanese “Application” Button', + 'Japanese “Passing Grade” Button', + 'Japanese “Vacancy” Button', + 'Japanese “Congratulations” Button', + 'Japanese “Secret” Button', + 'Japanese “Open for Business” Button', + 'Japanese “No Vacancy” Button', + 'Red Circle', + 'Blue Circle', + 'Black Circle', + 'White Circle', + 'Black Large Square', + 'White Large Square', + 'Black Medium Square', + 'White Medium Square', + 'Black Medium-Small Square', + 'White Medium-Small Square', + 'Black Small Square', + 'White Small Square', + 'Large Orange Diamond', + 'Large Blue Diamond', + 'Small Orange Diamond', + 'Small Blue Diamond', + 'Red Triangle Pointed Up', + 'Red Triangle Pointed Down', + 'Diamond With a Dot', + 'White Square Button', + 'Black Square Button' +], [ + '💘', + '💝', + '💖', + '💗', + '💓', + '💞', + '💕', + '💟', + '❣', + '💔', + '❤', + '🧡', + '💛', + '💚', + '💙', + '💜', + '🖤', + '💯', + '💢', + '💬', + '👁️‍🗨️', + '🗯', + '💭', + '💤', + '💮', + '♨', + '💈', + '🛑', + '🕛', + '🕧', + '🕐', + '🕜', + '🕑', + '🕝', + '🕒', + '🕞', + '🕓', + '🕟', + '🕔', + '🕠', + '🕕', + '🕡', + '🕖', + '🕢', + '🕗', + '🕣', + '🕘', + '🕤', + '🕙', + '🕥', + '🕚', + '🕦', + '🌀', + '♠', + '♥', + '♦', + '♣', + '🃏', + '🀄', + '🎴', + '🔇', + '🔈', + '🔉', + '🔊', + '📢', + '📣', + '📯', + '🔔', + '🔕', + '🎵', + '🎶', + '🏧', + '🚮', + '🚰', + '♿', + '🚹', + '🚺', + '🚻', + '🚼', + '🚾', + '⚠', + '🚸', + '⛔', + '🚫', + '🚳', + '🚭', + '🚯', + '🚱', + '🚷', + '🔞', + '☢', + '☣', + '⬆', + '↗', + '➡', + '↘', + '⬇', + '↙', + '⬅', + '↖', + '↕', + '↔', + '↩', + '↪', + '⤴', + '⤵', + '🔃', + '🔄', + '🔙', + '🔚', + '🔛', + '🔜', + '🔝', + '🛐', + '⚛', + '🕉', + '✡', + '☸', + '☯', + '✝', + '☦', + '☪', + '☮', + '🕎', + '🔯', + '♈', + '♉', + '♊', + '♋', + '♌', + '♍', + '♎', + '♏', + '♐', + '♑', + '♒', + '♓', + '⛎', + '🔀', + '🔁', + '🔂', + '▶', + '⏩', + '◀', + '⏪', + '🔼', + '⏫', + '🔽', + '⏬', + '⏹', + '⏏', + '🎦', + '🔅', + '🔆', + '📶', + '📳', + '📴', + '♾', + '♻', + '🔱', + '📛', + '🔰', + '⭕', + '✅', + '☑', + '✔', + '✖', + '❌', + '❎', + '➕', + '➖', + '➗', + '➰', + '➿', + '〽', + '✳', + '✴', + '❇', + '‼', + '⁉', + '❓', + '❔', + '❕', + '❗', + '©', + '®', + '™', + '#️⃣', + '0️⃣', + '1️⃣', + '2️⃣', + '3️⃣', + '4️⃣', + '5️⃣', + '6️⃣', + '7️⃣', + '8️⃣', + '9️⃣', + '🔟', + '🔠', + '🔡', + '🔢', + '🔣', + '🔤', + '🅰', + '🆎', + '🅱', + '🆑', + '🆒', + '🆓', + 'ℹ', + '🆔', + 'Ⓜ', + '🆕', + '🆖', + '🅾', + '🆗', + '🅿', + '🆘', + '🆙', + '🆚', + '🈁', + '🈂', + '🈷', + '🈶', + '🈯', + '🉐', + '🈹', + '🈚', + '🈲', + '🉑', + '🈸', + '🈴', + '🈳', + '㊗', + '㊙', + '🈺', + '🈵', + '🔴', + '🔵', + '⚫', + '⚪', + '⬛', + '⬜', + '◼', + '◻', + '◾', + '◽', + '▪', + '▫', + '🔶', + '🔷', + '🔸', + '🔹', + '🔺', + '🔻', + '💠', + '🔳', + '🔲' +]); + +/// Map of all possible emojis along with their names in [Category.FLAGS] +final Map flags = Map.fromIterables([ + 'Chequered Flag', + 'Triangular Flag', + 'Crossed Flags', + 'Black Flag', + 'White Flag', + 'Rainbow Flag', + 'Pirate Flag', + 'Flag: Ascension Island', + 'Flag: Andorra', + 'Flag: United Arab Emirates', + 'Flag: Afghanistan', + 'Flag: Antigua & Barbuda', + 'Flag: Anguilla', + 'Flag: Albania', + 'Flag: Armenia', + 'Flag: Angola', + 'Flag: Antarctica', + 'Flag: argentina', + 'Flag: American Samoa', + 'Flag: Austria', + 'Flag: Australia', + 'Flag: Aruba', + 'Flag: Åland Islands', + 'Flag: Azerbaijan', + 'Flag: Bosnia & Herzegovina', + 'Flag: Barbados', + 'Flag: Bangladesh', + 'Flag: Belgium', + 'Flag: Burkina Faso', + 'Flag: Bulgaria', + 'Flag: Bahrain', + 'Flag: Burundi', + 'Flag: Benin', + 'Flag: St. Barthélemy', + 'Flag: Bermuda', + 'Flag: Brunei', + 'Flag: Bolivia', + 'Flag: Caribbean Netherlands', + 'Flag: Brazil', + 'Flag: Bahamas', + 'Flag: Bhutan', + 'Flag: Bouvet Island', + 'Flag: Botswana', + 'Flag: Belarus', + 'Flag: Belize', + 'Flag: Canada', + 'Flag: Cocos (Keeling) Islands', + 'Flag: Congo - Kinshasa', + 'Flag: Central African Republic', + 'Flag: Congo - Brazzaville', + 'Flag: Switzerland', + 'Flag: Côte d’Ivoire', + 'Flag: Cook Islands', + 'Flag: Chile', + 'Flag: Cameroon', + 'Flag: China', + 'Flag: Colombia', + 'Flag: Clipperton Island', + 'Flag: Costa Rica', + 'Flag: Cuba', + 'Flag: Cape Verde', + 'Flag: Curaçao', + 'Flag: Christmas Island', + 'Flag: Cyprus', + 'Flag: Czechia', + 'Flag: Germany', + 'Flag: Diego Garcia', + 'Flag: Djibouti', + 'Flag: Denmark', + 'Flag: Dominica', + 'Flag: Dominican Republic', + 'Flag: Algeria', + 'Flag: Ceuta & Melilla', + 'Flag: Ecuador', + 'Flag: Estonia', + 'Flag: Egypt', + 'Flag: Western Sahara', + 'Flag: Eritrea', + 'Flag: Spain', + 'Flag: Ethiopia', + 'Flag: European Union', + 'Flag: Finland', + 'Flag: Fiji', + 'Flag: Falkland Islands', + 'Flag: Micronesia', + 'Flag: Faroe Islands', + 'Flag: france', + 'Flag: Gabon', + 'Flag: United Kingdom', + 'Flag: Grenada', + 'Flag: Georgia', + 'Flag: French Guiana', + 'Flag: Guernsey', + 'Flag: Ghana', + 'Flag: Gibraltar', + 'Flag: Greenland', + 'Flag: Gambia', + 'Flag: Guinea', + 'Flag: Guadeloupe', + 'Flag: Equatorial Guinea', + 'Flag: Greece', + 'Flag: South Georgia & South Sandwich Islands', + 'Flag: Guatemala', + 'Flag: Guam', + 'Flag: Guinea-Bissau', + 'Flag: Guyana', + 'Flag: Hong Kong SAR China', + 'Flag: Heard & McDonald Islands', + 'Flag: Honduras', + 'Flag: Croatia', + 'Flag: Haiti', + 'Flag: Hungary', + 'Flag: Canary Islands', + 'Flag: Indonesia', + 'Flag: Ireland', + 'Flag: Israel', + 'Flag: Isle of Man', + 'Flag: India', + 'Flag: British Indian Ocean Territory', + 'Flag: Iraq', + 'Flag: Iran', + 'Flag: Iceland', + 'Flag: Italy', + 'Flag: Jersey', + 'Flag: Jamaica', + 'Flag: Jordan', + 'Flag: Japan', + 'Flag: Kenya', + 'Flag: Kyrgyzstan', + 'Flag: Cambodia', + 'Flag: Kiribati', + 'Flag: Comoros', + 'Flag: St. Kitts & Nevis', + 'Flag: North Korea', + 'Flag: South Korea', + 'Flag: Kuwait', + 'Flag: Cayman Islands', + 'Flag: Kazakhstan', + 'Flag: Laos', + 'Flag: Lebanon', + 'Flag: St. Lucia', + 'Flag: Liechtenstein', + 'Flag: Sri Lanka', + 'Flag: Liberia', + 'Flag: Lesotho', + 'Flag: Lithuania', + 'Flag: Luxembourg', + 'Flag: Latvia', + 'Flag: Libya', + 'Flag: Morocco', + 'Flag: Monaco', + 'Flag: Moldova', + 'Flag: Montenegro', + 'Flag: St. Martin', + 'Flag: Madagascar', + 'Flag: Marshall Islands', + 'Flag: North Macedonia', + 'Flag: Mali', + 'Flag: Myanmar (Burma)', + 'Flag: Mongolia', + 'Flag: Macau Sar China', + 'Flag: Northern Mariana Islands', + 'Flag: Martinique', + 'Flag: Mauritania', + 'Flag: Montserrat', + 'Flag: Malta', + 'Flag: Mauritius', + 'Flag: Maldives', + 'Flag: Malawi', + 'Flag: Mexico', + 'Flag: Malaysia', + 'Flag: Mozambique', + 'Flag: Namibia', + 'Flag: New Caledonia', + 'Flag: Niger', + 'Flag: Norfolk Island', + 'Flag: Nigeria', + 'Flag: Nicaragua', + 'Flag: Netherlands', + 'Flag: Norway', + 'Flag: Nepal', + 'Flag: Nauru', + 'Flag: Niue', + 'Flag: New Zealand', + 'Flag: Oman', + 'Flag: Panama', + 'Flag: Peru', + 'Flag: French Polynesia', + 'Flag: Papua New Guinea', + 'Flag: Philippines', + 'Flag: Pakistan', + 'Flag: Poland', + 'Flag: St. Pierre & Miquelon', + 'Flag: Pitcairn Islands', + 'Flag: Puerto Rico', + 'Flag: Palestinian Territories', + 'Flag: Portugal', + 'Flag: Palau', + 'Flag: Paraguay', + 'Flag: Qatar', + 'Flag: Réunion', + 'Flag: Romania', + 'Flag: Serbia', + 'Flag: Russia', + 'Flag: Rwanda', + 'Flag: Saudi Arabia', + 'Flag: Solomon Islands', + 'Flag: Seychelles', + 'Flag: Sudan', + 'Flag: Sweden', + 'Flag: Singapore', + 'Flag: St. Helena', + 'Flag: Slovenia', + 'Flag: Svalbard & Jan Mayen', + 'Flag: Slovakia', + 'Flag: Sierra Leone', + 'Flag: San Marino', + 'Flag: Senegal', + 'Flag: Somalia', + 'Flag: Suriname', + 'Flag: South Sudan', + 'Flag: São Tomé & Príncipe', + 'Flag: El Salvador', + 'Flag: Sint Maarten', + 'Flag: Syria', + 'Flag: Swaziland', + 'Flag: Tristan Da Cunha', + 'Flag: Turks & Caicos Islands', + 'Flag: Chad', + 'Flag: French Southern Territories', + 'Flag: Togo', + 'Flag: Thailand', + 'Flag: Tajikistan', + 'Flag: Tokelau', + 'Flag: Timor-Leste', + 'Flag: Turkmenistan', + 'Flag: Tunisia', + 'Flag: Tonga', + 'Flag: Turkey', + 'Flag: Trinidad & Tobago', + 'Flag: Tuvalu', + 'Flag: Taiwan', + 'Flag: Tanzania', + 'Flag: Ukraine', + 'Flag: Uganda', + 'Flag: U.S. Outlying Islands', + 'Flag: United Nations', + 'Flag: United States', + 'Flag: Uruguay', + 'Flag: Uzbekistan', + 'Flag: Vatican City', + 'Flag: St. Vincent & Grenadines', + 'Flag: Venezuela', + 'Flag: British Virgin Islands', + 'Flag: U.S. Virgin Islands', + 'Flag: Vietnam', + 'Flag: Vanuatu', + 'Flag: Wallis & Futuna', + 'Flag: Samoa', + 'Flag: Kosovo', + 'Flag: Yemen', + 'Flag: Mayotte', + 'Flag: South Africa', + 'Flag: Zambia', + 'Flag: Zimbabwe' +], [ + '🏁', + '🚩', + '🎌', + '🏴', + '🏳', + '🏳️‍🌈', + '🏴‍☠️', + '🇦🇨', + '🇦🇩', + '🇦🇪', + '🇦🇫', + '🇦🇬', + '🇦🇮', + '🇦🇱', + '🇦🇲', + '🇦🇴', + '🇦🇶', + '🇦🇷', + '🇦🇸', + '🇦🇹', + '🇦🇺', + '🇦🇼', + '🇦🇽', + '🇦🇿', + '🇧🇦', + '🇧🇧', + '🇧🇩', + '🇧🇪', + '🇧🇫', + '🇧🇬', + '🇧🇭', + '🇧🇮', + '🇧🇯', + '🇧🇱', + '🇧🇲', + '🇧🇳', + '🇧🇴', + '🇧🇶', + '🇧🇷', + '🇧🇸', + '🇧🇹', + '🇧🇻', + '🇧🇼', + '🇧🇾', + '🇧🇿', + '🇨🇦', + '🇨🇨', + '🇨🇩', + '🇨🇫', + '🇨🇬', + '🇨🇭', + '🇨🇮', + '🇨🇰', + '🇨🇱', + '🇨🇲', + '🇨🇳', + '🇨🇴', + '🇨🇵', + '🇨🇷', + '🇨🇺', + '🇨🇻', + '🇨🇼', + '🇨🇽', + '🇨🇾', + '🇨🇿', + '🇩🇪', + '🇩🇬', + '🇩🇯', + '🇩🇰', + '🇩🇲', + '🇩🇴', + '🇩🇿', + '🇪🇦', + '🇪🇨', + '🇪🇪', + '🇪🇬', + '🇪🇭', + '🇪🇷', + '🇪🇸', + '🇪🇹', + '🇪🇺', + '🇫🇮', + '🇫🇯', + '🇫🇰', + '🇫🇲', + '🇫🇴', + '🇫🇷', + '🇬🇦', + '🇬🇧', + '🇬🇩', + '🇬🇪', + '🇬🇫', + '🇬🇬', + '🇬🇭', + '🇬🇮', + '🇬🇱', + '🇬🇲', + '🇬🇳', + '🇬🇵', + '🇬🇶', + '🇬🇷', + '🇬🇸', + '🇬🇹', + '🇬🇺', + '🇬🇼', + '🇬🇾', + '🇭🇰', + '🇭🇲', + '🇭🇳', + '🇭🇷', + '🇭🇹', + '🇭🇺', + '🇮🇨', + '🇮🇩', + '🇮🇪', + '🇮🇱', + '🇮🇲', + '🇮🇳', + '🇮🇴', + '🇮🇶', + '🇮🇷', + '🇮🇸', + '🇮🇹', + '🇯🇪', + '🇯🇲', + '🇯🇴', + '🇯🇵', + '🇰🇪', + '🇰🇬', + '🇰🇭', + '🇰🇮', + '🇰🇲', + '🇰🇳', + '🇰🇵', + '🇰🇷', + '🇰🇼', + '🇰🇾', + '🇰🇿', + '🇱🇦', + '🇱🇧', + '🇱🇨', + '🇱🇮', + '🇱🇰', + '🇱🇷', + '🇱🇸', + '🇱🇹', + '🇱🇺', + '🇱🇻', + '🇱🇾', + '🇲🇦', + '🇲🇨', + '🇲🇩', + '🇲🇪', + '🇲🇫', + '🇲🇬', + '🇲🇭', + '🇲🇰', + '🇲🇱', + '🇲🇲', + '🇲🇳', + '🇲🇴', + '🇲🇵', + '🇲🇶', + '🇲🇷', + '🇲🇸', + '🇲🇹', + '🇲🇺', + '🇲🇻', + '🇲🇼', + '🇲🇽', + '🇲🇾', + '🇲🇿', + '🇳🇦', + '🇳🇨', + '🇳🇪', + '🇳🇫', + '🇳🇬', + '🇳🇮', + '🇳🇱', + '🇳🇴', + '🇳🇵', + '🇳🇷', + '🇳🇺', + '🇳🇿', + '🇴🇲', + '🇵🇦', + '🇵🇪', + '🇵🇫', + '🇵🇬', + '🇵🇭', + '🇵🇰', + '🇵🇱', + '🇵🇲', + '🇵🇳', + '🇵🇷', + '🇵🇸', + '🇵🇹', + '🇵🇼', + '🇵🇾', + '🇶🇦', + '🇷🇪', + '🇷🇴', + '🇷🇸', + '🇷🇺', + '🇷🇼', + '🇸🇦', + '🇸🇧', + '🇸🇨', + '🇸🇩', + '🇸🇪', + '🇸🇬', + '🇸🇭', + '🇸🇮', + '🇸🇯', + '🇸🇰', + '🇸🇱', + '🇸🇲', + '🇸🇳', + '🇸🇴', + '🇸🇷', + '🇸🇸', + '🇸🇹', + '🇸🇻', + '🇸🇽', + '🇸🇾', + '🇸🇿', + '🇹🇦', + '🇹🇨', + '🇹🇩', + '🇹🇫', + '🇹🇬', + '🇹🇭', + '🇹🇯', + '🇹🇰', + '🇹🇱', + '🇹🇲', + '🇹🇳', + '🇹🇴', + '🇹🇷', + '🇹🇹', + '🇹🇻', + '🇹🇼', + '🇹🇿', + '🇺🇦', + '🇺🇬', + '🇺🇲', + '🇺🇳', + '🇺🇸', + '🇺🇾', + '🇺🇿', + '🇻🇦', + '🇻🇨', + '🇻🇪', + '🇻🇬', + '🇻🇮', + '🇻🇳', + '🇻🇺', + '🇼🇫', + '🇼🇸', + '🇽🇰', + '🇾🇪', + '🇾🇹', + '🇿🇦', + '🇿🇲', + '🇿🇼' +]); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker.dart new file mode 100644 index 0000000000..0a8793a5d5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker.dart @@ -0,0 +1,338 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'models/category_models.dart'; +import 'config.dart'; +import 'default_emoji_picker_view.dart'; +import 'models/emoji_model.dart'; +import 'emoji_lists.dart' as emoji_list; +import 'emoji_view_state.dart'; +import 'models/recent_emoji_model.dart'; + +/// All the possible categories that [Emoji] can be put into +/// +/// All [Category] are shown in the category bar +enum Category { + /// Searched emojis + SEARCH, + + /// Recent emojis + RECENT, + + /// Smiley emojis + SMILEYS, + + /// Animal emojis + ANIMALS, + + /// Food emojis + FOODS, + + /// Activity emojis + ACTIVITIES, + + /// Travel emojis + TRAVEL, + + /// Objects emojis + OBJECTS, + + /// Sumbol emojis + SYMBOLS, + + /// Flag emojis + FLAGS, +} + +/// Enum to alter the keyboard button style +enum ButtonMode { + /// Android button style - gives the button a splash color with ripple effect + MATERIAL, + + /// iOS button style - gives the button a fade out effect when pressed + CUPERTINO +} + +/// Callback function for when emoji is selected +/// +/// The function returns the selected [Emoji] as well +/// as the [Category] from which it originated +typedef OnEmojiSelected = void Function(Category category, Emoji emoji); + +/// Callback function for backspace button +typedef OnBackspacePressed = void Function(); + +/// Callback function for custom view +typedef EmojiViewBuilder = Widget Function(Config config, EmojiViewState state); + +/// The Emoji Keyboard widget +/// +/// This widget displays a grid of [Emoji] sorted by [Category] +/// which the user can horizontally scroll through. +/// +/// There is also a bottombar which displays all the possible [Category] +/// and allow the user to quickly switch to that [Category] +class EmojiPicker extends StatefulWidget { + /// EmojiPicker for flutter + const EmojiPicker({ + Key? key, + required this.onEmojiSelected, + this.onBackspacePressed, + this.config = const Config(), + this.customWidget, + }) : super(key: key); + + /// Custom widget + final EmojiViewBuilder? customWidget; + + /// The function called when the emoji is selected + final OnEmojiSelected onEmojiSelected; + + /// The function called when backspace button is pressed + final OnBackspacePressed? onBackspacePressed; + + /// Config for customizations + final Config config; + + @override + EmojiPickerState createState() => EmojiPickerState(); +} + +class EmojiPickerState extends State { + static const platform = MethodChannel('emoji_picker_flutter'); + + List categoryEmoji = List.empty(growable: true); + List recentEmoji = List.empty(growable: true); + late Future updateEmojiFuture; + + // Prevent emojis to be reloaded with every build + bool loaded = false; + + @override + void initState() { + super.initState(); + updateEmojiFuture = _updateEmojis(); + } + + @override + void didUpdateWidget(covariant EmojiPicker oldWidget) { + if (oldWidget.config != widget.config) { + // Config changed - rebuild EmojiPickerView completely + loaded = false; + updateEmojiFuture = _updateEmojis(); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + if (!loaded) { + // Load emojis + updateEmojiFuture.then( + (value) => WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + loaded = true; + }); + }), + ); + + // Show loading indicator + return const Center(child: CircularProgressIndicator()); + } + if (widget.config.showRecentsTab) { + categoryEmoji[0].emoji = + recentEmoji.map((e) => e.emoji).toList().cast(); + } + + final state = EmojiViewState( + categoryEmoji, + _getOnEmojiListener(), + widget.onBackspacePressed, + ); + + // Build + return widget.customWidget == null + ? DefaultEmojiPickerView(widget.config, state) + : widget.customWidget!(widget.config, state); + } + + // Add recent emoji handling to tap listener + OnEmojiSelected _getOnEmojiListener() { + return (category, emoji) { + if (widget.config.showRecentsTab) { + _addEmojiToRecentlyUsed(emoji).then((value) { + if (category != Category.RECENT && mounted) { + setState(() { + // rebuild to update recent emoji tab + // when it is not current tab + }); + } + }); + } + widget.onEmojiSelected(category, emoji); + }; + } + + // Initialize emoji data + Future _updateEmojis() async { + categoryEmoji.clear(); + if (widget.config.showRecentsTab) { + recentEmoji = await _getRecentEmojis(); + final List recentEmojiMap = + recentEmoji.map((e) => e.emoji).toList().cast(); + categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap)); + } + categoryEmoji.addAll([ + CategoryEmoji( + Category.SMILEYS, + await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'), + ), + CategoryEmoji( + Category.ANIMALS, + await _getAvailableEmojis(emoji_list.animals, title: 'animals'), + ), + CategoryEmoji( + Category.FOODS, + await _getAvailableEmojis(emoji_list.foods, title: 'foods'), + ), + CategoryEmoji( + Category.ACTIVITIES, + await _getAvailableEmojis( + emoji_list.activities, + title: 'activities', + ), + ), + CategoryEmoji( + Category.TRAVEL, + await _getAvailableEmojis(emoji_list.travel, title: 'travel'), + ), + CategoryEmoji( + Category.OBJECTS, + await _getAvailableEmojis(emoji_list.objects, title: 'objects'), + ), + CategoryEmoji( + Category.SYMBOLS, + await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'), + ), + CategoryEmoji( + Category.FLAGS, + await _getAvailableEmojis(emoji_list.flags, title: 'flags'), + ) + ]); + } + + // Get available emoji for given category title + Future> _getAvailableEmojis( + Map map, { + required String title, + }) async { + Map? newMap; + + // Get Emojis cached locally if available + newMap = await _restoreFilteredEmojis(title); + + if (newMap == null) { + // Check if emoji is available on this platform + newMap = await _getPlatformAvailableEmoji(map); + // Save available Emojis to local storage for faster loading next time + if (newMap != null) { + await _cacheFilteredEmojis(title, newMap); + } + } + + // Map to Emoji Object + return newMap!.entries + .map((entry) => Emoji(entry.key, entry.value)) + .toList(); + } + + // Check if emoji is available on current platform + Future?> _getPlatformAvailableEmoji( + Map emoji, + ) async { + if (Platform.isAndroid) { + Map? filtered = {}; + const delimiter = '|'; + try { + final entries = emoji.values.join(delimiter); + final keys = emoji.keys.join(delimiter); + final result = (await platform.invokeMethod( + 'checkAvailability', + {'emojiKeys': keys, 'emojiEntries': entries}, + )) as String; + final resultKeys = result.split(delimiter); + for (var i = 0; i < resultKeys.length; i++) { + filtered[resultKeys[i]] = emoji[resultKeys[i]]!; + } + } on PlatformException catch (_) { + filtered = null; + } + return filtered; + } else { + return emoji; + } + } + + // Restore locally cached emoji + Future?> _restoreFilteredEmojis(String title) async { + final prefs = await SharedPreferences.getInstance(); + final emojiJson = prefs.getString(title); + if (emojiJson == null) { + return null; + } + final emojis = + Map.from(jsonDecode(emojiJson) as Map); + return emojis; + } + + // Stores filtered emoji locally for faster access next time + Future _cacheFilteredEmojis( + String title, + Map emojis, + ) async { + final prefs = await SharedPreferences.getInstance(); + final emojiJson = jsonEncode(emojis); + prefs.setString(title, emojiJson); + } + + // Returns list of recently used emoji from cache + Future> _getRecentEmojis() async { + final prefs = await SharedPreferences.getInstance(); + final emojiJson = prefs.getString('recent'); + if (emojiJson == null) { + return []; + } + final json = jsonDecode(emojiJson) as List; + return json.map(RecentEmoji.fromJson).toList(); + } + + // Add an emoji to recently used list or increase its counter + Future _addEmojiToRecentlyUsed(Emoji emoji) async { + final prefs = await SharedPreferences.getInstance(); + final recentEmojiIndex = + recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji); + if (recentEmojiIndex != -1) { + // Already exist in recent list + // Just update counter + recentEmoji[recentEmojiIndex].counter++; + } else { + recentEmoji.add(RecentEmoji(emoji, 1)); + } + // Sort by counter desc + recentEmoji.sort((a, b) => b.counter - a.counter); + // Limit entries to recentsLimit + recentEmoji = recentEmoji.sublist( + 0, + min(widget.config.recentsLimit, recentEmoji.length), + ); + // save locally + prefs.setString('recent', jsonEncode(recentEmoji)); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker_builder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker_builder.dart new file mode 100644 index 0000000000..fe526e01db --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker_builder.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import 'config.dart'; +import 'emoji_view_state.dart'; + +/// Template class for custom implementation +/// Inherit this class to create your own EmojiPicker +abstract class EmojiPickerBuilder extends StatefulWidget { + /// Constructor + const EmojiPickerBuilder(this.config, this.state, {Key? key}) + : super(key: key); + + /// Config for customizations + final Config config; + + /// State that holds current emoji data + final EmojiViewState state; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart new file mode 100644 index 0000000000..202f913715 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart @@ -0,0 +1,21 @@ +import 'models/category_models.dart'; +import 'emoji_picker.dart'; + +/// State that holds current emoji data +class EmojiViewState { + /// Constructor + EmojiViewState( + this.categoryEmoji, + this.onEmojiSelected, + this.onBackspacePressed, + ); + + /// List of all category including their emoji + final List categoryEmoji; + + /// Callback when pressed on emoji + final OnEmojiSelected onEmojiSelected; + + /// Callback when pressed on backspace + final OnBackspacePressed? onBackspacePressed; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/category_models.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/category_models.dart new file mode 100644 index 0000000000..885ff2ab0c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/category_models.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import 'emoji_model.dart'; +import '../emoji_picker.dart'; + +/// Container for Category and their emoji +class CategoryEmoji { + /// Constructor + CategoryEmoji(this.category, this.emoji); + + /// Category instance + final Category category; + + /// List of emoji of this category + List emoji; + + @override + String toString() { + return 'Name: $category, Emoji: $emoji'; + } +} + +/// Class that defines the icon representing a [Category] +class CategoryIcon { + /// Icon of Category + const CategoryIcon({ + required this.icon, + this.color = const Color(0xffd3d3d3), + this.selectedColor = const Color(0xffb2b2b2), + }); + + /// The icon to represent the category + final IconData icon; + + /// The default color of the icon + final Color color; + + /// The color of the icon once the category is selected + final Color selectedColor; +} + +/// Class used to define all the [CategoryIcon] shown for each [Category] +/// +/// This allows the keyboard to be personalized by changing icons shown. +/// If a [CategoryIcon] is set as null or not defined during initialization, +/// the default icons will be used instead +class CategoryIcons { + /// Constructor + const CategoryIcons({ + this.recentIcon = Icons.access_time, + this.smileyIcon = Icons.tag_faces, + this.animalIcon = Icons.pets, + this.foodIcon = Icons.fastfood, + this.activityIcon = Icons.directions_run, + this.travelIcon = Icons.location_city, + this.objectIcon = Icons.lightbulb_outline, + this.symbolIcon = Icons.emoji_symbols, + this.flagIcon = Icons.flag, + this.searchIcon = Icons.search, + }); + + /// Icon for [Category.RECENT] + final IconData recentIcon; + + /// Icon for [Category.SMILEYS] + final IconData smileyIcon; + + /// Icon for [Category.ANIMALS] + final IconData animalIcon; + + /// Icon for [Category.FOODS] + final IconData foodIcon; + + /// Icon for [Category.ACTIVITIES] + final IconData activityIcon; + + /// Icon for [Category.TRAVEL] + final IconData travelIcon; + + /// Icon for [Category.OBJECTS] + final IconData objectIcon; + + /// Icon for [Category.SYMBOLS] + final IconData symbolIcon; + + /// Icon for [Category.FLAGS] + final IconData flagIcon; + + /// Icon for [Category.SEARCH] + final IconData searchIcon; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/emoji_model.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/emoji_model.dart new file mode 100644 index 0000000000..a1808a9419 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/emoji_model.dart @@ -0,0 +1,32 @@ +/// A class to store data for each individual emoji +class Emoji { + /// Emoji constructor + const Emoji(this.name, this.emoji); + + /// The name or description for this emoji + final String name; + + /// The unicode string for this emoji + /// + /// This is the string that should be displayed to view the emoji + final String emoji; + + @override + String toString() { + // return 'Name: $name, Emoji: $emoji'; + return name; + } + + /// Parse Emoji from json + static Emoji fromJson(Map json) { + return Emoji(json['name'] as String, json['emoji'] as String); + } + + /// Encode Emoji to json + Map toJson() { + return { + 'name': name, + 'emoji': emoji, + }; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/recent_emoji_model.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/recent_emoji_model.dart new file mode 100644 index 0000000000..1571bc9a92 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/emoji_picker/src/models/recent_emoji_model.dart @@ -0,0 +1,30 @@ +import 'emoji_model.dart'; + +/// Class that holds an recent emoji +/// Recent Emoji has an instance of the emoji +/// And a counter, which counts how often this emoji +/// has been used before +class RecentEmoji { + /// Constructor + RecentEmoji(this.emoji, this.counter); + + /// Emoji instance + final Emoji emoji; + + /// Counter how often emoji has been used before + int counter = 0; + + /// Parse RecentEmoji from json + static RecentEmoji fromJson(dynamic json) { + return RecentEmoji( + Emoji.fromJson(json['emoji'] as Map), + json['counter'] as int, + ); + } + + /// Encode RecentEmoji to json + Map toJson() => { + 'emoji': emoji, + 'counter': counter, + }; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart deleted file mode 100644 index 6e1c377277..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/favorite/favorite_bloc.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/hover.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ViewFavoriteButton extends StatelessWidget { - const ViewFavoriteButton({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final isFavorite = state.views.any((v) => v.item.id == view.id); - return Listener( - onPointerDown: (_) => - context.read().add(FavoriteEvent.toggle(view)), - child: FlowyTooltip( - message: isFavorite - ? LocaleKeys.button_removeFromFavorites.tr() - : LocaleKeys.button_addToFavorites.tr(), - child: FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: const EdgeInsets.all(6), - child: FlowySvg( - isFavorite ? FlowySvgs.favorited_s : FlowySvgs.favorite_s, - size: const Size.square(18), - blendMode: isFavorite ? null : BlendMode.srcIn, - ), - ), - ), - ), - ); - }, - ); - } -} 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..06dce358da 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 @@ -1,54 +1,35 @@ -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/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/widget/flowy_tooltip.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/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:device_info_plus/device_info_plus.dart'; class QuestionBubble extends StatelessWidget { - const QuestionBubble({super.key}); + const QuestionBubble({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return const SizedBox.square( - dimension: 32.0, + return const SizedBox( + width: 30, + height: 30, child: BubbleActionList(), ); } } -class BubbleActionList extends StatefulWidget { - const BubbleActionList({super.key}); - - @override - State createState() => _BubbleActionListState(); -} - -class _BubbleActionListState extends State { - bool isOpen = false; - - Color get fontColor => isOpen - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.tertiary; - - Color get fillColor => isOpen - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.tertiaryContainer; - - void toggle() { - setState(() { - isOpen = !isOpen; - }); - } +class BubbleActionList extends StatelessWidget { + const BubbleActionList({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -56,97 +37,45 @@ class _BubbleActionListState extends State { actions.addAll( BubbleAction.values.map((action) => BubbleActionWrapper(action)), ); - - actions.add(SocialMediaSection()); - actions.add(FlowyVersionSection()); - - final (color, borderColor, shadowColor, iconColor) = - Theme.of(context).isLightMode - ? ( - Colors.white, - const Color(0x2D454849), - const Color(0x14000000), - Colors.black, - ) - : ( - const Color(0xFF242B37), - const Color(0x2DFFFFFF), - const Color(0x14000000), - Colors.white, - ); + actions.add(FlowyVersionDescription()); return PopoverActionList( 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(), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: ShapeDecoration( - color: color, - shape: RoundedRectangleBorder( - side: BorderSide(width: 0.50, color: borderColor), - borderRadius: BorderRadius.circular(18), - ), - shadows: [ - BoxShadow( - color: shadowColor, - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: FlowySvg( - FlowySvgs.help_center_s, - color: iconColor, - ), - ), - onTap: () => controller.show(), - ), - ), + return FlowyTextButton( + '?', + tooltip: LocaleKeys.questionBubble_help.tr(), + fontWeight: FontWeight.w600, + fontColor: Theme.of(context).colorScheme.tertiary, + fillColor: Theme.of(context).colorScheme.tertiaryContainer, + hoverColor: Theme.of(context).colorScheme.tertiaryContainer, + mainAxisAlignment: MainAxisAlignment.center, + radius: Corners.s10Border, + onPressed: () => controller.show(), ); }, - onClosed: toggle, onSelected: (action, controller) { if (action is BubbleActionWrapper) { switch (action.inner) { case BubbleAction.whatsNews: - afLaunchUrlString('https://www.appflowy.io/what-is-new'); + _launchURL("https://www.appflowy.io/whatsnew"); break; - case BubbleAction.getSupport: - afLaunchUrlString('https://discord.gg/9Q2xaN37tV'); + case BubbleAction.help: + _launchURL("https://discord.gg/9Q2xaN37tV"); break; case BubbleAction.debug: _DebugToast().show(); break; case BubbleAction.shortcuts: - afLaunchUrlString( - 'https://docs.appflowy.io/docs/appflowy/product/shortcuts', + _launchURL( + "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts", ); break; case BubbleAction.markdown: - afLaunchUrlString( - 'https://docs.appflowy.io/docs/appflowy/product/markdown', - ); - break; - case BubbleAction.github: - afLaunchUrlString( - 'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose', - ); - break; - case BubbleAction.helpAndDocumentation: - afLaunchUrlString( - 'https://appflowy.com/guide', + _launchURL( + "https://appflowy.gitbook.io/docs/essential-documentation/markdown", ); break; } @@ -156,14 +85,23 @@ class _BubbleActionListState extends State { }, ); } + + _launchURL(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + throw 'Could not launch $url'; + } + } } class _DebugToast { void show() async { - String debugInfo = ''; + var debugInfo = ""; debugInfo += await _getDeviceInfo(); debugInfo += await _getDocumentPath(); - await Clipboard.setData(ClipboardData(text: debugInfo)); + Clipboard.setData(ClipboardData(text: debugInfo)); showMessageToast(LocaleKeys.questionBubble_debug_success.tr()); } @@ -173,33 +111,72 @@ 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}"); } 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) { + 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( + mainAxisAlignment: MainAxisAlignment.start, + 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); + } + }, + ); + } } -class BubbleActionWrapper extends ActionCell { - BubbleActionWrapper(this.inner); +enum BubbleAction { whatsNews, help, debug, shortcuts, markdown } +class BubbleActionWrapper extends ActionCell { final BubbleAction inner; + + BubbleActionWrapper(this.inner); @override - Widget? leftIcon(Color iconColor) => inner.icons; + Widget? leftIcon(Color iconColor) => FlowyText.regular(inner.emoji); @override String get name => inner.name; @@ -210,40 +187,29 @@ 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: return LocaleKeys.questionBubble_shortcuts.tr(); case BubbleAction.markdown: return LocaleKeys.questionBubble_markdown.tr(); - case BubbleAction.github: - return LocaleKeys.questionBubble_feedback.tr(); } } - Widget? get icons { + String 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 '🆕'; + case BubbleAction.help: + return '👥'; case BubbleAction.debug: - return const FlowySvg(FlowySvgs.debug_s); + return '🐛'; case BubbleAction.shortcuts: - return const FlowySvg(FlowySvgs.keyboard_s); + return '📋'; case BubbleAction.markdown: - return const FlowySvg(FlowySvgs.number_s); - case BubbleAction.github: - return const FlowySvg(FlowySvgs.share_feedback_s); + return '✨'; } } } 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/left_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart new file mode 100644 index 0000000000..7aeed651af --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart @@ -0,0 +1,84 @@ +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; + +class ViewLeftBarItem extends StatefulWidget { + final ViewPB view; + + ViewLeftBarItem({required this.view, Key? key}) + : super(key: ValueKey(view.hashCode)); + + @override + State createState() => _ViewLeftBarItemState(); +} + +class _ViewLeftBarItemState extends State { + final _controller = TextEditingController(); + final _focusNode = FocusNode(); + late final ViewListener _viewListener; + late ViewPB view; + + @override + void initState() { + super.initState(); + view = widget.view; + _focusNode.addListener(_handleFocusChanged); + _viewListener = ViewListener(viewId: widget.view.id); + _viewListener.start( + onViewUpdated: (updatedView) { + if (mounted) { + setState(() { + view = updatedView; + _controller.text = view.name; + }); + } + }, + ); + _controller.text = view.name; + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.removeListener(_handleFocusChanged); + _focusNode.dispose(); + _viewListener.stop(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + key: ValueKey(_controller.text), + onDoubleTap: () { + _controller.selection = TextSelection( + baseOffset: 0, + extentOffset: _controller.text.length, + ); + }, + child: TextField( + controller: _controller, + focusNode: _focusNode, + scrollPadding: EdgeInsets.zero, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 4.0), + border: InputBorder.none, + isDense: true, + ), + style: Theme.of(context).textTheme.bodyMedium, + ), + ); + } + + void _handleFocusChanged() { + if (_controller.text.isEmpty) { + _controller.text = view.name; + return; + } + + if (_controller.text != view.name) { + ViewBackendService.updateView(viewId: view.id, name: _controller.text); + } + } +} 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 deleted file mode 100644 index 62b3ccc8f3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ /dev/null @@ -1,201 +0,0 @@ -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: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 MoreViewActions extends StatefulWidget { - const MoreViewActions({ - super.key, - required this.view, - this.customActions = const [], - }); - - /// 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; - - @override - State createState() => _MoreViewActionsState(); -} - -class _MoreViewActionsState extends State { - final popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @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, - 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; - } -} - -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, - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index 2ecec3244c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart +++ /dev/null @@ -1,148 +0,0 @@ -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: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 ViewAction extends StatelessWidget { - const ViewAction({ - super.key, - required this.type, - required this.view, - this.mutex, - }); - - final ViewMoreActionType type; - final ViewPB view; - final PopoverMutex? mutex; - - @override - Widget build(BuildContext context) { - final wrapper = ViewMoreActionTypeWrapper( - type, - view, - (controller, data) async { - await _onAction(context, data); - 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, - ), - ), - ), - ); - } -} 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 deleted file mode 100644 index 8e0fa8c43c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart +++ /dev/null @@ -1,54 +0,0 @@ -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: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 { - const FontSizeAction({super.key}); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.leftWithCenterAligned, - constraints: const BoxConstraints(maxHeight: 40, maxWidth: 240), - offset: const Offset(-10, 0), - popupBuilder: (context) { - return BlocBuilder( - builder: (_, state) => FontSizeStepper( - minimumValue: 10, - maximumValue: 24, - value: state.fontSize, - divisions: 8, - onChanged: (newFontSize) => context - .read() - .syncFontSize(newFontSize), - ), - ); - }, - 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, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart deleted file mode 100644 index 67261598ff..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; - -class FontSizeStepper extends StatefulWidget { - const FontSizeStepper({ - super.key, - required this.minimumValue, - required this.maximumValue, - required this.value, - required this.divisions, - required this.onChanged, - }); - - final double minimumValue; - final double maximumValue; - final double value; - final ValueChanged onChanged; - final int divisions; - - @override - State createState() => _FontSizeStepperState(); -} - -class _FontSizeStepperState extends State { - late double _value = widget.value.clamp( - widget.minimumValue, - widget.maximumValue, - ); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - children: [ - const FlowyText('A', fontSize: 14), - const HSpace(6), - Expanded( - child: SliderTheme( - data: Theme.of(context).sliderTheme.copyWith( - showValueIndicator: ShowValueIndicator.never, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 8, - ), - overlayShape: const RoundSliderOverlayShape( - overlayRadius: 16, - ), - ), - child: Slider( - value: _value, - min: widget.minimumValue, - max: widget.maximumValue, - divisions: widget.divisions, - onChanged: (value) { - setState(() => _value = value); - widget.onChanged(value); - }, - ), - ), - ), - const HSpace(6), - const FlowyText('A', fontSize: 20), - ], - ), - ); - } -} 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 deleted file mode 100644 index 27b96d39e9..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart +++ /dev/null @@ -1,81 +0,0 @@ -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'; -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({ - super.key, - 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 - Widget build(BuildContext context) { - final numberFormat = NumberFormat(); - - // If more info is added to this Widget, use a separated ListView - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (documentCounters != null && titleCounters != null) ...[ - FlowyText.regular( - LocaleKeys.moreAction_wordCount.tr( - args: [ - numberFormat - .format( - documentCounters!.wordCount + titleCounters!.wordCount, - ) - .toString(), - ], - ), - fontSize: 12, - color: Theme.of(context).hintColor, - ), - const VSpace(2), - FlowyText.regular( - LocaleKeys.moreAction_charCount.tr( - args: [ - numberFormat - .format( - documentCounters!.charCount + titleCounters!.charCount, - ) - .toString(), - ], - ), - fontSize: 12, - color: Theme.of(context).hintColor, - ), - ], - if (createdAt != null) ...[ - if (documentCounters != null && titleCounters != null) - const VSpace(2), - FlowyText.regular( - LocaleKeys.moreAction_createdAt.tr( - args: [dateFormat.formatDate(createdAt!, true, timeFormat)], - ), - fontSize: 12, - 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..6fc14bda64 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,13 +1,22 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:styled_widget/styled_widget.dart'; class PopoverActionList extends StatefulWidget { + final List actions; + final PopoverMutex? mutex; + final Function(T, PopoverController) onSelected; + final BoxConstraints constraints; + final PopoverDirection direction; + final Widget Function(PopoverController) buildChild; + final VoidCallback? onPopupBuilder; + final VoidCallback? onClosed; + final bool asBarrier; + final Offset offset; + const PopoverActionList({ - super.key, - this.controller, - this.popoverMutex, required this.actions, required this.buildChild, required this.onSelected, @@ -17,39 +26,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; - final Function(T, PopoverController) onSelected; - final PopoverMutex? mutex; - final VoidCallback? onClosed; - final VoidCallback? onPopupBuilder; - final PopoverDirection direction; - 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; + Key? key, + }) : super(key: key); @override State> createState() => _PopoverActionListState(); @@ -57,37 +40,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 +61,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) { @@ -109,24 +74,21 @@ class _PopoverActionListState ); } else if (action is PopoverActionCell) { return PopoverActionCellWidget( - popoverMutex: widget.popoverMutex, popoverController: popoverController, action: action, itemHeight: ActionListSizes.itemHeight, ); } else { final custom = action as CustomActionCell; - return custom.buildWithContext( - context, - popoverController, - widget.popoverMutex, - ); + return custom.buildWithContext(context); } }).toList(); return IntrinsicHeight( child: IntrinsicWidth( - child: Column(children: children), + child: Column( + children: children, + ), ), ); }, @@ -139,9 +101,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 +118,7 @@ abstract class PopoverActionCell extends PopoverAction { } abstract class CustomActionCell extends PopoverAction { - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ); + Widget buildWithContext(BuildContext context); } abstract class PopoverAction {} @@ -176,16 +131,15 @@ class ActionListSizes { } class ActionCellWidget extends StatelessWidget { - const ActionCellWidget({ - super.key, - required this.action, - required this.onSelected, - required this.itemHeight, - }); - final T action; final Function(T) onSelected; final double itemHeight; + const ActionCellWidget({ + Key? key, + required this.action, + required this.onSelected, + required this.itemHeight, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -201,7 +155,6 @@ class ActionCellWidget extends StatelessWidget { leftIcon: leftIcon, rightIcon: rightIcon, name: actionCell.name, - textColor: actionCell.textColor(context), onTap: () => onSelected(action), ); } @@ -210,13 +163,11 @@ class ActionCellWidget extends StatelessWidget { class PopoverActionCellWidget extends StatefulWidget { const PopoverActionCellWidget({ super.key, - this.popoverMutex, required this.popoverController, required this.action, required this.itemHeight, }); - final PopoverMutex? popoverMutex; final T action; final double itemHeight; @@ -238,7 +189,6 @@ class _PopoverActionCellWidgetState final rightIcon = actionCell.rightIcon(Theme.of(context).colorScheme.onSurface); return AppFlowyPopover( - mutex: widget.popoverMutex, controller: popoverController, asBarrier: true, popupBuilder: (context) => actionCell.builder( @@ -262,10 +212,9 @@ class HoverButton extends StatelessWidget { super.key, required this.onTap, required this.itemHeight, - this.leftIcon, + required this.leftIcon, required this.name, - this.rightIcon, - this.textColor, + required this.rightIcon, }); final VoidCallback onTap; @@ -273,7 +222,6 @@ class HoverButton extends StatelessWidget { final Widget? leftIcon; final Widget? rightIcon; final String name; - final Color? textColor; @override Widget build(BuildContext context) { @@ -287,14 +235,12 @@ class HoverButton extends StatelessWidget { children: [ if (leftIcon != null) ...[ leftIcon!, - HSpace(ActionListSizes.itemHPadding), + HSpace(ActionListSizes.itemHPadding) ], Expanded( - child: FlowyText.regular( + child: FlowyText.medium( 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 deleted file mode 100644 index 954fc77603..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ /dev/null @@ -1,111 +0,0 @@ -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.name, - required this.popoverController, - required this.emoji, - this.icon, - this.showIconChanger = true, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], - }); - - final ViewPB view; - final String name; - final PopoverController popoverController; - final EmojiIconData emoji; - final Widget? icon; - final bool showIconChanger; - final List tabs; - - @override - State createState() => _RenameViewPopoverState(); -} - -class _RenameViewPopoverState extends State { - final TextEditingController _controller = TextEditingController(); - - @override - void initState() { - super.initState(); - _controller.text = widget.name; - _controller.selection = - TextSelection(baseOffset: 0, extentOffset: widget.name.length); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.showIconChanger) ...[ - SizedBox( - width: 30.0, - child: EmojiPickerButton( - emoji: widget.emoji, - defaultIcon: widget.icon, - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 18), - onSubmitted: _updateViewIcon, - documentId: widget.view.id, - tabs: widget.tabs, - ), - ), - const HSpace(6), - ], - SizedBox( - height: 32.0, - width: 220, - child: FlowyTextField( - controller: _controller, - maxLength: 256, - onSubmitted: _updateViewName, - onCanceled: () => _updateViewName(_controller.text), - showCounter: false, - ), - ), - ], - ); - } - - Future _updateViewName(String name) async { - if (name.isNotEmpty && name != widget.name) { - await ViewBackendService.updateView( - viewId: widget.view.id, - name: _controller.text, - ); - widget.popoverController.close(); - } - } - - Future _updateViewIcon( - SelectedEmojiIconResult r, - PopoverController? _, - ) async { - await ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: r.data, - ); - if (!r.keepOpen) { - 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 deleted file mode 100644 index 88474b20b3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/tab_bar_item.dart +++ /dev/null @@ -1,70 +0,0 @@ -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, - }); - - final ViewPB view; - final bool shortForm; - - @override - State createState() => _ViewTabBarItemState(); -} - -class _ViewTabBarItemState extends State { - late final ViewListener _viewListener; - late ViewPB view; - - @override - void initState() { - super.initState(); - view = widget.view; - _viewListener = ViewListener(viewId: widget.view.id); - _viewListener.start( - onViewUpdated: (updatedView) { - if (mounted) { - setState(() => view = updatedView); - } - }, - ); - } - - @override - void dispose() { - _viewListener.stop(); - super.dispose(); - } - - @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, - ), - ), - ], - ], - ); - } -} 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..80335c66fe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart @@ -1,63 +1,34 @@ +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(), - this.thumbColor, - this.activeBackgroundColor, - this.inactiveBackgroundColor, - this.duration = const Duration(milliseconds: 150), - this.padding = const EdgeInsets.all(8.0), - }); - - final bool value; - final void Function(bool) onChanged; final ToggleStyle style; + final bool value; final Color? thumbColor; final Color? activeBackgroundColor; final Color? inactiveBackgroundColor; + final void Function(bool) onChanged; final EdgeInsets padding; - final Duration duration; + + const Toggle({ + Key? key, + required this.value, + required this.onChanged, + required this.style, + this.thumbColor, + this.activeBackgroundColor, + this.inactiveBackgroundColor, + this.padding = const EdgeInsets.all(8.0), + }) : super(key: key); @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..62664d83c0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart @@ -0,0 +1,18 @@ +class ToggleStyle { + final double height; + final double width; + + final double thumbRadius; + + ToggleStyle({ + required this.height, + required this.width, + required this.thumbRadius, + }); + + static ToggleStyle get big => + ToggleStyle(height: 16, width: 27, thumbRadius: 14); + + static ToggleStyle get small => + ToggleStyle(height: 10, width: 16, thumbRadius: 8); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart deleted file mode 100644 index 347d95d01d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -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:flutter/material.dart'; -import 'package:string_validator/string_validator.dart'; - -class UserAvatar extends StatelessWidget { - const UserAvatar({ - super.key, - required this.iconUrl, - required this.name, - required this.size, - required this.fontSize, - this.isHovering = false, - this.decoration, - }); - - final String iconUrl; - final String name; - final double size; - final double fontSize; - final Decoration? decoration; - - // If true, a border will be applied on top of the avatar - final bool isHovering; - - @override - Widget build(BuildContext context) { - if (iconUrl.isEmpty) { - return _buildEmptyAvatar(context); - } else if (isURL(iconUrl)) { - return _buildUrlAvatar(context); - } else { - return _buildEmojiAvatar(context); - } - } - - Widget _buildEmptyAvatar(BuildContext context) { - final String nameOrDefault = _userName(name); - final Color color = ColorGenerator(name).toColor(); - const initialsCount = 2; - - // Taking the first letters of the name components and limiting to 2 elements - final nameInitials = nameOrDefault - .split(' ') - .where((element) => element.isNotEmpty) - .take(initialsCount) - .map((element) => element[0].toUpperCase()) - .join(); - - return Container( - width: size, - height: size, - alignment: Alignment.center, - decoration: decoration ?? - BoxDecoration( - color: color, - shape: BoxShape.circle, - border: isHovering - ? Border.all( - color: _darken(color), - width: 4, - ) - : null, - ), - child: FlowyText.medium( - nameInitials, - color: Colors.black, - fontSize: fontSize, - ), - ); - } - - Widget _buildUrlAvatar(BuildContext context) { - return SizedBox.square( - dimension: size, - child: DecoratedBox( - decoration: decoration ?? - BoxDecoration( - shape: BoxShape.circle, - border: isHovering - ? Border.all( - color: Theme.of(context).colorScheme.primary, - width: 4, - ) - : 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), - ), - ), - ), - ); - } - - Widget _buildEmojiAvatar(BuildContext context) { - return SizedBox.square( - dimension: size, - child: DecoratedBox( - decoration: 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, fontSize: fontSize), - ), - ), - ), - ); - } - - /// Return the user name, if the user name is empty, - /// return the default user 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/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart deleted file mode 100644 index 3be0973123..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ /dev/null @@ -1,475 +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/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:flowy_infra_ui/flowy_infra_ui.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 -class ViewTitleBar extends StatelessWidget { - const ViewTitleBar({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => ViewTitleBarBloc(view: view)), - BlocProvider( - create: (_) => ViewLockStatusBloc(view: view) - ..add(const ViewLockStatusEvent.initial()), - ), - ], - child: BlocBuilder( - builder: (context, state) { - final ancestors = state.ancestors; - if (ancestors.isEmpty) { - return const SizedBox.shrink(); - } - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - height: 24, - child: Row( - children: [ - ..._buildViewTitles( - context, - ancestors, - state.isDeleted, - ), - _buildLockPageStatus(context), - ], - ), - ), - ); - }, - ), - ); - } - - 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); - } - - // 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] - // if the views are [root, view1, view2, view3], show [root, view1, view2, view3] - const lowerBound = 2; - final upperBound = views.length - 2; - bool hasAddedEllipsis = false; - final children = []; - - 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; i++) { - final view = views[i]; - - if (i >= lowerBound && i < upperBound) { - if (!hasAddedEllipsis) { - hasAddedEllipsis = true; - children.addAll([ - const FlowyText.regular(' ... '), - const FlowySvg(FlowySvgs.title_bar_divider_s), - ]); - } - continue; - } - - final child = FlowyTooltip( - key: ValueKey(view.id), - message: view.name, - 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 - onUpdated: () { - context - .read() - .add(const ViewTitleBarEvent.reload()); - }, - ), - ); - - children.add(child); - - if (i != views.length - 1) { - // if not the last one, add a divider - children.add(const FlowySvg(FlowySvgs.title_bar_divider_s)); - } - } - 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 { - editable, - uneditable, -} - -class ViewTitle extends StatefulWidget { - const ViewTitle({ - super.key, - required this.view, - this.behavior = ViewTitleBehavior.editable, - required this.onUpdated, - }); - - final ViewPB view; - final ViewTitleBehavior behavior; - final VoidCallback onUpdated; - - @override - State createState() => _ViewTitleState(); -} - -class _ViewTitleState extends State { - final popoverController = PopoverController(); - final textEditingController = TextEditingController(); - - @override - void dispose() { - textEditingController.dispose(); - popoverController.close(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - 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(); - }, - builder: (context, state) { - // root view - if (widget.view.parentViewId.isEmpty) { - return Row( - children: [ - FlowyText.regular(state.name), - const HSpace(4.0), - ], - ); - } else if (widget.view.isSpace) { - return _buildSpaceTitle(context, state); - } else if (isEditable) { - return _buildEditableViewTitle(context, state); - } else { - return _buildUnEditableViewTitle(context, state); - } - }, - ), - ); - } - - Widget _buildSpaceTitle(BuildContext context, ViewTitleState state) { - return Container( - alignment: Alignment.center, - margin: const EdgeInsets.symmetric(horizontal: 6.0), - child: _buildIconAndName(context, state, false), - ); - } - - Widget _buildUnEditableViewTitle(BuildContext context, ViewTitleState state) { - return Listener( - onPointerDown: (_) => context.read().openPlugin(widget.view), - child: SizedBox( - height: 32.0, - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric(horizontal: 6.0), - text: _buildIconAndName(context, state, false), - ), - ), - ); - } - - Widget _buildEditableViewTitle(BuildContext context, ViewTitleState state) { - return AppFlowyPopover( - constraints: const BoxConstraints( - maxWidth: 300, - maxHeight: 44, - ), - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 6), - popupBuilder: (context) { - // icon + textfield - _resetTextEditingController(state); - return RenameViewPopover( - view: widget.view, - name: widget.view.name, - popoverController: popoverController, - icon: widget.view.defaultIcon(), - emoji: state.icon, - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], - ); - }, - child: SizedBox( - height: 32.0, - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric(horizontal: 6.0), - text: _buildIconAndName(context, state, true), - ), - ), - ); - } - - Widget _buildIconAndName( - BuildContext context, - ViewTitleState state, - bool isEditable, - ) { - final spaceIcon = state.view?.buildSpaceIconSvg(context); - return SingleChildScrollView( - child: Row( - children: [ - if (state.icon.isNotEmpty) ...[ - RawEmojiIconWidget(emoji: state.icon, emojiSize: 14.0), - const HSpace(4.0), - ], - if (state.view?.isSpace == true && spaceIcon != null) ...[ - SpaceIcon( - dimension: 14, - svgSize: 8.5, - space: state.view!, - cornerRadius: 4, - ), - const HSpace(6.0), - ], - Opacity( - opacity: isEditable ? 1.0 : 0.5, - child: FlowyText.regular( - state.name.isEmpty - ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() - : state.name, - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 18.0, - ), - ), - ], - ), - ); - } - - void _resetTextEditingController(ViewTitleState state) { - textEditingController - ..text = state.name - ..selection = TextSelection( - baseOffset: 0, - extentOffset: state.name.length, - ); - } -} - -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/CMakeLists.txt b/frontend/appflowy_flutter/linux/CMakeLists.txt index b9f7cce174..3c6927375c 100644 --- a/frontend/appflowy_flutter/linux/CMakeLists.txt +++ b/frontend/appflowy_flutter/linux/CMakeLists.txt @@ -55,7 +55,6 @@ apply_standard_settings(${BINARY_NAME}) target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) add_dependencies(${BINARY_NAME} flutter_assemble) - # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of @@ -69,11 +68,11 @@ set_target_properties(${BINARY_NAME} # them to the application. include(flutter/generated_plugins.cmake) + # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") - if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() diff --git a/frontend/appflowy_flutter/linux/appflowy.desktop.temp b/frontend/appflowy_flutter/linux/appflowy.desktop.temp new file mode 100644 index 0000000000..2b189ef243 --- /dev/null +++ b/frontend/appflowy_flutter/linux/appflowy.desktop.temp @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=AppFlowy +Comment=An Open Source Alternative to Notion +Icon=[CHANGE_THIS]/AppFlowy/flowy_logo.svg +Exec=[CHANGE_THIS]/AppFlowy/AppFlowy +Categories=Office +Type=Application +Terminal=false \ No newline at end of file diff --git a/frontend/appflowy_flutter/linux/flutter/dart_ffi/binding.h b/frontend/appflowy_flutter/linux/flutter/dart_ffi/binding.h index 78992141ca..3d42bf55ce 100644 --- a/frontend/appflowy_flutter/linux/flutter/dart_ffi/binding.h +++ b/frontend/appflowy_flutter/linux/flutter/dart_ffi/binding.h @@ -3,7 +3,7 @@ #include #include -int64_t init_sdk(int64_t port, char *data); +int64_t init_sdk(char *path); void async_event(int64_t port, const uint8_t *input, uintptr_t len); @@ -11,10 +11,8 @@ const uint8_t *sync_event(const uint8_t *input, uintptr_t len); int32_t set_stream_port(int64_t port); -int32_t set_log_stream_port(int64_t port); - void link_me_please(void); -void rust_log(int64_t level, const char *data); +void backend_log(int64_t level, const char *data); void set_env(const char *data); diff --git a/frontend/appflowy_flutter/linux/my_application.cc b/frontend/appflowy_flutter/linux/my_application.cc index 2a3a02cac4..490ead4cd0 100644 --- a/frontend/appflowy_flutter/linux/my_application.cc +++ b/frontend/appflowy_flutter/linux/my_application.cc @@ -19,16 +19,40 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) static void my_application_activate(GApplication *application) { MyApplication *self = MY_APPLICATION(application); - - GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); - if (windows) { - gtk_window_present(GTK_WINDOW(windows->data)); - return; - } - 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)); @@ -63,7 +87,7 @@ static gboolean my_application_local_command_line(GApplication *application, gch g_application_activate(application); *exit_status = 0; - return FALSE; + return TRUE; } // Implements GObject::dispose. @@ -83,9 +107,10 @@ static void my_application_class_init(MyApplicationClass *klass) static void my_application_init(MyApplication *self) {} -MyApplication* my_application_new() { +MyApplication *my_application_new() +{ return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, + "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } 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 b/frontend/appflowy_flutter/macos/Podfile index 38ec205fff..715baa2d38 100644 --- a/frontend/appflowy_flutter/macos/Podfile +++ b/frontend/appflowy_flutter/macos/Podfile @@ -1,6 +1,4 @@ -MINIMUM_MACOSX_DEPLOYMENT_TARGET_SUPPORTED = '10.14' - -platform :osx, MINIMUM_MACOSX_DEPLOYMENT_TARGET_SUPPORTED +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -29,31 +27,18 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_macos_podfile_setup def build_specify_archs_only - if ENV.has_key?('BUILD_ACTIVE_ARCHS_ONLY') - xcodeproj_path = File.dirname(__FILE__) + '/Runner.xcodeproj' - project = Xcodeproj::Project.open(xcodeproj_path) - project.targets.each do |target| - if target.name == 'Runner' - target.build_configurations.each do |config| - config.build_settings['ONLY_ACTIVE_ARCH'] = ENV['BUILD_ACTIVE_ARCHS_ONLY'] - end - end - end - project.save() - end - - if ENV.has_key?('BUILD_ARCHS') - xcodeproj_path = File.dirname(__FILE__) + '/Runner.xcodeproj' - project = Xcodeproj::Project.open(xcodeproj_path) - project.targets.each do |target| - if target.name == 'Runner' - target.build_configurations.each do |config| - config.build_settings['ARCHS'] = ENV['BUILD_ARCHS'] - end - end - end - project.save() - end + # if ENV.has_key?('BUILD_ARCHS') + # xcodeproj_path = File.dirname(__FILE__) + '/Runner.xcodeproj' + # project = Xcodeproj::Project.open(xcodeproj_path) + # project.targets.each do |target| + # if target.name == 'Runner' + # target.build_configurations.each do |config| + # config.build_settings['ARCHS'] = ENV['BUILD_ARCHS'] + # end + # end + # end + # project.save() + # end end build_specify_archs_only() @@ -69,21 +54,4 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end - - installer.aggregate_targets.each do |target| - target.xcconfigs.each do |variant, xcconfig| - xcconfig_path = target.client_root + target.xcconfig_relative_path(variant) - IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR")) - end - end - - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference - xcconfig_path = config.base_configuration_reference.real_path - IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR")) - end - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = MINIMUM_MACOSX_DEPLOYMENT_TARGET_SUPPORTED - end - end end diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock deleted file mode 100644 index b4a1a3d20d..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: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 - auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c - irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 - local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 - Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 - webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 - -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..de6bf2b7c8 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 */, ); @@ -214,7 +206,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -435,15 +427,12 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - ENABLE_HARDENED_RUNTIME = NO; EXCLUDED_ARCHS = ""; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AppFlowy; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; PRODUCT_NAME = AppFlowy; @@ -572,15 +561,12 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - ENABLE_HARDENED_RUNTIME = NO; EXCLUDED_ARCHS = ""; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AppFlowy; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; PRODUCT_NAME = AppFlowy; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -600,15 +586,12 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - ENABLE_HARDENED_RUNTIME = YES; EXCLUDED_ARCHS = ""; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AppFlowy; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; PRODUCT_NAME = AppFlowy; diff --git a/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 74866db678..656a4d8b2a 100644 --- a/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ 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..656857119f 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 © 2023 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..ae544967cf 100644 --- a/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements +++ b/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements @@ -1,20 +1,12 @@ - - 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.cs.allow-jit + + 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..43954508aa 100644 --- a/frontend/appflowy_flutter/macos/Runner/Info.plist +++ b/frontend/appflowy_flutter/macos/Runner/Info.plist @@ -1,61 +1,55 @@ - - 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 + + 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 + + io.appflowy.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..8e357d7ca1 100644 --- a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift +++ b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -private let kTrafficLightOffetTop = 14 +private let kTrafficLightOffetTop = 22 class MainFlutterWindow: NSWindow { func registerMethodChannel(flutterViewController: FlutterViewController) { @@ -17,7 +17,7 @@ class MainFlutterWindow: NSWindow { let nY = position[1] as! NSNumber let x = nX.doubleValue let y = nY.doubleValue - + self.setFrameOrigin(NSPoint(x: x, y: y)) result(nil) return @@ -30,7 +30,7 @@ class MainFlutterWindow: NSWindow { result(nil) return } - + result(FlutterMethodNotImplemented) }) } @@ -51,9 +51,9 @@ class MainFlutterWindow: NSWindow { let zoomButton = self.standardWindowButton(ButtonType.zoomButton)! let titlebarView = closeButton.superview! - self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 12) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 30) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 48) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 20) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 38) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 56) let customToolbar = NSTitlebarAccessoryViewController() let newView = NSView() @@ -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..f1e49b4f10 100644 --- a/frontend/appflowy_flutter/macos/Runner/Release.entitlements +++ b/frontend/appflowy_flutter/macos/Runner/Release.entitlements @@ -1,20 +1,10 @@ - - 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.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/android/build.gradle b/frontend/appflowy_flutter/packages/appflowy_backend/android/build.gradle index 0090800f92..0601cc381e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/android/build.gradle +++ b/frontend/appflowy_flutter/packages/appflowy_backend/android/build.gradle @@ -2,14 +2,14 @@ group 'com.plugin.appflowy_backend' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.8.0' + ext.kotlin_version = '1.6.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,13 +25,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 33 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 23 + minSdkVersion 16 } } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/build.gradle b/frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/build.gradle index b96b43daa3..aa5daf1e9d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/build.gradle +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/build.gradle @@ -35,8 +35,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.plugin.flowy_sdk_example" - minSdkVersion 33 - targetSdkVersion 33 + minSdkVersion 16 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } 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..d60ec71028 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.11' # 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/example/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_backend/example/pubspec.yaml index 665892c800..6faa9793ce 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - flutter_lints: ^3.0.1 + flutter_lints: ^2.0.1 # 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_backend/ios/Classes/binding.h b/frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/binding.h deleted file mode 100644 index 78992141ca..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/binding.h +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include -#include -#include - -int64_t init_sdk(int64_t port, char *data); - -void async_event(int64_t port, const uint8_t *input, uintptr_t len); - -const uint8_t *sync_event(const uint8_t *input, uintptr_t len); - -int32_t set_stream_port(int64_t port); - -int32_t set_log_stream_port(int64_t port); - -void link_me_please(void); - -void rust_log(int64_t level, const char *data); - -void set_env(const char *data); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/ios/appflowy_backend.podspec b/frontend/appflowy_flutter/packages/appflowy_backend/ios/appflowy_backend.podspec index 138013d18b..429afd46fe 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/ios/appflowy_backend.podspec +++ b/frontend/appflowy_flutter/packages/appflowy_backend/ios/appflowy_backend.podspec @@ -22,5 +22,4 @@ A new flutter plugin project. s.swift_version = '5.0' s.static_framework = true s.vendored_libraries = "libdart_ffi.a" - s.library = "c++" end 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..a31faedf9d 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,13 @@ -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 'ffi.dart' as ffi; - export 'package:async/async.dart'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:async'; +import 'package:appflowy_backend/rust_stream.dart'; +import 'package:flutter/services.dart'; +import 'dart:ffi'; +import 'env_serde.dart'; +import 'ffi.dart' as ffi; +import 'package:ffi/ffi.dart'; enum ExceptionType { AppearanceSettingsIsEmpty, @@ -32,55 +27,18 @@ class FlowySDK { FlowySDK(); - Future dispose() async {} + void dispose() {} + + Future init(Directory sdkDir) async { + final port = RustStreamReceiver.shared.port; + ffi.set_stream_port(port); - Future init(String configuration) async { - ffi.set_stream_port(RustStreamReceiver.shared.port); ffi.store_dart_post_cobject(NativeApi.postCObject); + ffi.init_sdk(sdkDir.path.toNativeUtf8()); + } - // On iOS, VSCode can't print logs from Rust, so we need to use a different method to print logs. - // So we use a shared port to receive logs from Rust and print them using the logger. In release mode, we don't print logs. - if (Platform.isIOS && kDebugMode) { - ffi.set_log_stream_port(RustLogStreamReceiver.logShared.port); - } - - // final completer = Completer(); - // // Create a SendPort that accepts only one message. - // final sendPort = singleCompletePort(completer); - - final code = ffi.init_sdk(0, configuration.toNativeUtf8()); - if (code != 0) { - throw Exception('Failed to initialize the SDK'); - } - // return completer.future; - } -} - -class RustLogStreamReceiver { - static RustLogStreamReceiver logShared = RustLogStreamReceiver._internal(); - late RawReceivePort _ffiPort; - late StreamController _streamController; - late StreamSubscription _subscription; - int get port => _ffiPort.sendPort.nativePort; - - RustLogStreamReceiver._internal() { - _ffiPort = RawReceivePort(); - _streamController = StreamController(); - _ffiPort.handler = _streamController.add; - - _subscription = _streamController.stream.listen((data) { - String decodedString = utf8.decode(data); - Log.info(decodedString); - }); - } - - factory RustLogStreamReceiver() { - return logShared; - } - - Future dispose() async { - await _streamController.close(); - await _subscription.cancel(); - _ffiPort.close(); + void setEnv(AppFlowyEnv env) { + final jsonStr = jsonEncode(env.toJson()); + ffi.set_env(jsonStr.toNativeUtf8()); } } 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..4f911e42e2 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart @@ -1,42 +1,37 @@ -import 'dart:async'; -import 'dart:convert' show utf8; import 'dart:ffi'; -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; - -import 'package:appflowy_backend/ffi.dart' as ffi; +import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; // ignore: unnecessary_import import 'package:appflowy_backend/protobuf/dart-ffi/ffi_response.pb.dart'; -import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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-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_result/appflowy_result.dart'; -import 'package:ffi/ffi.dart'; +import 'package:appflowy_backend/protobuf/flowy-net/network_state.pb.dart'; import 'package:isolates/isolates.dart'; import 'package:isolates/ports.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/services.dart'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:appflowy_backend/ffi.dart' as ffi; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; + +// ignore: unused_import import 'package:protobuf/protobuf.dart'; - -import '../protobuf/flowy-date/entities.pb.dart'; -import '../protobuf/flowy-date/event_map.pb.dart'; - +import 'dart:convert' show utf8; +import '../protobuf/flowy-config/entities.pb.dart'; +import '../protobuf/flowy-config/event_map.pb.dart'; +import '../protobuf/flowy-net/event_map.pb.dart'; import 'error.dart'; -part 'dart_event/flowy-folder/dart_event.dart'; +part 'dart_event/flowy-folder2/dart_event.dart'; +part 'dart_event/flowy-net/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-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-document2/dart_event.dart'; +part 'dart_event/flowy-config/dart_event.dart'; enum FFIException { RequestIsEmpty, @@ -48,8 +43,7 @@ class DispatchException implements Exception { } class Dispatch { - static Future> asyncRequest( - FFIRequest request) { + static Future> asyncRequest(FFIRequest request) { // FFIRequest => Rust SDK final bytesFuture = _sendToRust(request); @@ -63,45 +57,43 @@ class Dispatch { } } -Future> _extractPayload( - Future> responseFuture) { +Future> _extractPayload( + Future> responseFuture) { return responseFuture.then((result) { return result.fold( (response) { switch (response.code) { case FFIStatusCode.Ok: - return FlowySuccess(Uint8List.fromList(response.payload)); + return left(Uint8List.fromList(response.payload)); case FFIStatusCode.Err: - final errorBytes = Uint8List.fromList(response.payload); - GlobalErrorCodeNotifier.receiveErrorBytes(errorBytes); - return FlowyFailure(errorBytes); + return right(Uint8List.fromList(response.payload)); case FFIStatusCode.Internal: final error = utf8.decode(response.payload); Log.error("Dispatch internal error: $error"); - return FlowyFailure(emptyBytes()); + return right(emptyBytes()); default: Log.error("Impossible to here"); - return FlowyFailure(emptyBytes()); + return right(emptyBytes()); } }, (error) { Log.error("Response should not be empty $error"); - return FlowyFailure(emptyBytes()); + return right(emptyBytes()); }, ); }); } -Future> _extractResponse( +Future> _extractResponse( Completer bytesFuture) { return bytesFuture.future.then((bytes) { try { final response = FFIResponse.fromBuffer(bytes); - return FlowySuccess(response); + return left(response); } catch (e, s) { final error = StackTraceError(e, s); Log.error('Deserialize response failed. ${error.toString()}'); - return FlowyFailure(error.asFlowyError()); + return right(error.asFlowyError()); } }); } 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/env_serde.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart new file mode 100644 index 0000000000..957476dbd2 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart @@ -0,0 +1,64 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'env_serde.l.dart'; + +@JsonSerializable() +class AppFlowyEnv { + final SupabaseConfiguration supabase_config; + final SupabaseDBConfig supabase_db_config; + + AppFlowyEnv( + {required this.supabase_config, required this.supabase_db_config}); + + factory AppFlowyEnv.fromJson(Map json) => + _$AppFlowyEnvFromJson(json); + + Map toJson() => _$AppFlowyEnvToJson(this); +} + +@JsonSerializable() +class SupabaseConfiguration { + final String url; + final String key; + final String jwt_secret; + + SupabaseConfiguration( + {required this.url, required this.key, required this.jwt_secret}); + + factory SupabaseConfiguration.fromJson(Map json) => + _$SupabaseConfigurationFromJson(json); + + Map toJson() => _$SupabaseConfigurationToJson(this); +} + +@JsonSerializable() +class SupabaseDBConfig { + final String url; + final String key; + final String jwt_secret; + final CollabTableConfig collab_table_config; + + SupabaseDBConfig( + {required this.url, + required this.key, + required this.jwt_secret, + required this.collab_table_config}); + + factory SupabaseDBConfig.fromJson(Map json) => + _$SupabaseDBConfigFromJson(json); + + Map toJson() => _$SupabaseDBConfigToJson(this); +} + +@JsonSerializable() +class CollabTableConfig { + final String table_name; + final bool enable; + + CollabTableConfig({required this.table_name, required this.enable}); + + factory CollabTableConfig.fromJson(Map json) => + _$CollabTableConfigFromJson(json); + + Map toJson() => _$CollabTableConfigToJson(this); +} diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.l.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.l.dart new file mode 100644 index 0000000000..8fe70f71d9 --- /dev/null +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.l.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'env_serde.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppFlowyEnv _$AppFlowyEnvFromJson(Map json) => AppFlowyEnv( + supabase_config: SupabaseConfiguration.fromJson( + json['supabase_config'] as Map), + supabase_db_config: SupabaseDBConfig.fromJson( + json['supabase_db_config'] as Map), + ); + +Map _$AppFlowyEnvToJson(AppFlowyEnv instance) => + { + 'supabase_config': instance.supabase_config, + 'supabase_db_config': instance.supabase_db_config, + }; + +SupabaseConfiguration _$SupabaseConfigurationFromJson( + Map json) => + SupabaseConfiguration( + url: json['url'] as String, + key: json['key'] as String, + jwt_secret: json['jwt_secret'] as String, + ); + +Map _$SupabaseConfigurationToJson( + SupabaseConfiguration instance) => + { + 'url': instance.url, + 'key': instance.key, + 'jwt_secret': instance.jwt_secret, + }; + +SupabaseDBConfig _$SupabaseDBConfigFromJson(Map json) => + SupabaseDBConfig( + url: json['url'] as String, + key: json['key'] as String, + jwt_secret: json['jwt_secret'] as String, + collab_table_config: CollabTableConfig.fromJson( + json['collab_table_config'] as Map), + ); + +Map _$SupabaseDBConfigToJson(SupabaseDBConfig instance) => + { + 'url': instance.url, + 'key': instance.key, + 'jwt_secret': instance.jwt_secret, + 'collab_table_config': instance.collab_table_config, + }; + +CollabTableConfig _$CollabTableConfigFromJson(Map json) => + CollabTableConfig( + table_name: json['table_name'] as String, + enable: json['enable'] as bool, + ); + +Map _$CollabTableConfigToJson(CollabTableConfig instance) => + { + 'table_name': instance.table_name, + 'enable': instance.enable, + }; diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart index a1eb8947df..9d4e17cca1 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; @@ -78,20 +77,17 @@ typedef _invoke_sync_Dart = Pointer Function( /// C function `init_sdk`. int init_sdk( - int port, - Pointer data, + Pointer path, ) { - return _init_sdk(port, data); + return _init_sdk(path); } final _init_sdk_Dart _init_sdk = _dart_ffi_lib.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk'); typedef _init_sdk_C = Int64 Function( - Int64 port, Pointer path, ); typedef _init_sdk_Dart = int Function( - int port, Pointer path, ); @@ -111,22 +107,6 @@ typedef _set_stream_port_Dart = int Function( int port, ); -/// C function `set log stream port`. -int set_log_stream_port(int port) { - return _set_log_stream_port(port); -} - -final _set_log_stream_port_Dart _set_log_stream_port = _dart_ffi_lib - .lookupFunction<_set_log_stream_port_C, _set_log_stream_port_Dart>( - 'set_log_stream_port'); - -typedef _set_log_stream_port_C = Int32 Function( - Int64 port, -); -typedef _set_log_stream_port_Dart = int Function( - int port, -); - /// C function `link_me_please`. void link_me_please() { _link_me_please(); @@ -154,20 +134,20 @@ typedef _store_dart_post_cobject_Dart = void Function( Pointer)>> ptr, ); -void rust_log( +void log( int level, Pointer data, ) { - _invoke_rust_log(level, data); + _invoke_log(level, data); } -final _invoke_rust_log_Dart _invoke_rust_log = _dart_ffi_lib - .lookupFunction<_invoke_rust_log_C, _invoke_rust_log_Dart>('rust_log'); -typedef _invoke_rust_log_C = Void Function( +final _invoke_log_Dart _invoke_log = _dart_ffi_lib + .lookupFunction<_invoke_log_C, _invoke_log_Dart>('backend_log'); +typedef _invoke_log_C = Void Function( Int64 level, Pointer data, ); -typedef _invoke_rust_log_Dart = void Function( +typedef _invoke_log_Dart = void Function( int level, Pointer, ); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart index ce0a4e2248..0cb8e8ca7f 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart @@ -1,85 +1,67 @@ // 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 'package:ffi/ffi.dart' as ffi; 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; + 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 + ), ); } - // 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; + if (isReleaseVersion()) { + log(0, toNativeUtf8(msg)); + } else { + Log.shared._logger.i(msg, error, stackTrace); } - - _log(LogLevel.info, 0, msg, error, stackTrace); } static void debug(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; + if (isReleaseVersion()) { + log(1, toNativeUtf8(msg)); + } else { + Log.shared._logger.d(msg, error, stackTrace); } - - _log(LogLevel.debug, 1, msg, error, stackTrace); } static void warn(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; + if (isReleaseVersion()) { + log(3, toNativeUtf8(msg)); + } else { + Log.shared._logger.w(msg, error, stackTrace); } - - _log(LogLevel.warning, 3, msg, error, stackTrace); } static void trace(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; + if (isReleaseVersion()) { + log(2, toNativeUtf8(msg)); + } else { + Log.shared._logger.v(msg, error, stackTrace); } - - _log(LogLevel.verbose, 2, msg, error, stackTrace); } static void error(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; + if (isReleaseVersion()) { + log(4, toNativeUtf8(msg)); + } else { + Log.shared._logger.e(msg, error, stackTrace); } - - _log(LogLevel.error, 4, msg, error, stackTrace); } } @@ -87,22 +69,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/lib/rust_stream.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart index df904c2d19..02aa671ffa 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart @@ -1,9 +1,8 @@ -import 'dart:async'; -import 'dart:ffi'; import 'dart:isolate'; +import 'dart:async'; import 'dart:typed_data'; +import 'dart:ffi'; import 'package:appflowy_backend/log.dart'; - import 'protobuf/flowy-notification/subject.pb.dart'; typedef ObserverCallback = void Function(SubscribeObject observable); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/linux/Classes/binding.h b/frontend/appflowy_flutter/packages/appflowy_backend/linux/Classes/binding.h index c14543eec1..9a1769c338 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/linux/Classes/binding.h +++ b/frontend/appflowy_flutter/packages/appflowy_backend/linux/Classes/binding.h @@ -14,6 +14,6 @@ int32_t set_stream_port(int64_t port); void link_me_please(void); -void rust_log(int64_t level, const char *data); +void backend_log(int64_t level, const char *data); void set_env(const char *data); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h b/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h index 78992141ca..3d42bf55ce 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h +++ b/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h @@ -3,7 +3,7 @@ #include #include -int64_t init_sdk(int64_t port, char *data); +int64_t init_sdk(char *path); void async_event(int64_t port, const uint8_t *input, uintptr_t len); @@ -11,10 +11,8 @@ const uint8_t *sync_event(const uint8_t *input, uintptr_t len); int32_t set_stream_port(int64_t port); -int32_t set_log_stream_port(int64_t port); - void link_me_please(void); -void rust_log(int64_t level, const char *data); +void backend_log(int64_t level, const char *data); void set_env(const char *data); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/macos/appflowy_backend.podspec b/frontend/appflowy_flutter/packages/appflowy_backend/macos/appflowy_backend.podspec index 00d9b71180..19aa3dfaa7 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/macos/appflowy_backend.podspec +++ b/frontend/appflowy_flutter/packages/appflowy_backend/macos/appflowy_backend.podspec @@ -17,7 +17,7 @@ A new flutter plugin project. s.public_header_files = 'Classes/**/*.h' s.dependency 'FlutterMacOS' - s.platform = :osx, '10.13' + s.platform = :osx, '10.11' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' s.static_framework = true diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml index 18aea4838b..a2946ad335 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml @@ -13,17 +13,20 @@ dependencies: sdk: flutter ffi: ^2.0.2 isolates: ^3.0.3+8 - protobuf: ^3.1.0 - talker: ^4.7.1 + protobuf: ^2.0.0 + dartz: ^0.10.1 + freezed_annotation: + logger: ^1.0.0 plugin_platform_interface: ^2.1.3 - appflowy_result: - path: ../appflowy_result - fixnum: ^1.1.0 - async: ^2.11.0 + json_annotation: ^4.7.0 dev_dependencies: flutter_test: sdk: flutter + build_runner: + freezed: + flutter_lints: ^2.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..74f1645bd0 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,8 +1,8 @@ -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}); + const PopoverMenu({Key? key}) : super(key: key); @override State createState() => _PopoverMenuState(); @@ -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 { - const ExampleButton({ - super.key, - required this.label, - required this.direction, - this.offset = Offset.zero, - }); - final String label; final Offset? offset; - final PopoverDirection direction; + final PopoverDirection? direction; + + const ExampleButton({ + Key? key, + required this.label, + this.direction, + this.offset = Offset.zero, + }) : super(key: key); @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..d5f55866b0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart @@ -1,20 +1,29 @@ 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()); } class MyApp extends StatelessWidget { - const MyApp({super.key}); + const MyApp({Key? key}) : super(key: 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'), @@ -23,7 +32,16 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); + const MyHomePage({Key? key, required this.title}) : super(key: key); + + // 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; @@ -34,83 +52,78 @@ 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, - ), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ExampleButton( - label: 'Top', - offset: Offset(0, 10), - direction: PopoverDirection.bottomWithCenterAligned, - ), - Column( + body: Row(children: [ + Column(children: [ + const ExampleButton( + label: "Left top", + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithLeftAligned, + ), + Expanded(child: Container()), + const ExampleButton( + label: "Left bottom", + offset: Offset(0, -10), + direction: PopoverDirection.topWithLeftAligned, + ), + ]), + const Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ExampleButton( + label: "Top", + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithCenterAligned, + ), + Expanded( + child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ ExampleButton( - label: 'Central', + label: "Central", offset: Offset(0, 10), direction: PopoverDirection.bottomWithCenterAligned, ), ], ), - ExampleButton( - label: 'Bottom', - offset: Offset(0, -10), - direction: PopoverDirection.topWithCenterAligned, - ), - ], + ), + ExampleButton( + label: "Bottom", + offset: Offset(0, -10), + direction: PopoverDirection.topWithCenterAligned, + ), + ], + ), + ), + const Column( + children: [ + ExampleButton( + label: "Right top", + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithRightAligned, ), - 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, - ), - ], + 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 @@ -45,7 +45,7 @@ dev_dependencies: # 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. - flutter_lints: ^3.0.1 + flutter_lints: ^2.0.0 # 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/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..d63e486a1e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart @@ -2,101 +2,115 @@ import 'dart:collection'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.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; +class PopoverMask extends StatefulWidget { + final void Function() onTap; + final void Function()? onExit; final Decoration? decoration; + const PopoverMask( + {Key? key, required this.onTap, this.onExit, this.decoration}) + : super(key: key); + + @override + State createState() => _PopoverMaskState(); +} + +class _PopoverMaskState extends State { + @override + void initState() { + HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent); + super.initState(); + } + + bool _handleGlobalKeyEvent(KeyEvent event) { + if (event.logicalKey == LogicalKeyboardKey.escape && + event is KeyDownEvent) { + if (widget.onExit != null) { + widget.onExit!(); + } + return true; + } else { + return false; + } + } + + @override + void deactivate() { + HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent); + super.deactivate(); + } + @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap, + onTap: widget.onTap, child: Container( - decoration: decoration, + decoration: widget.decoration, ), ); } 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..fdb3628011 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -1,24 +1,24 @@ import 'package:appflowy_popover/src/layout.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'mask.dart'; import 'mutex.dart'; class PopoverController { PopoverState? _state; - void close() => _state?.close(); - void show() => _state?.showOverlay(); - void showAt(Offset position) => _state?.showOverlay(position); + close() { + _state?.close(); + } + + 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 { @@ -46,41 +46,7 @@ enum PopoverDirection { custom, } -enum PopoverClickHandler { - listener, - gestureDetector, -} - 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 @@ -105,97 +71,108 @@ class Popover extends StatefulWidget { /// The direction of the popover final PopoverDirection direction; - final VoidCallback? onOpen; - final VoidCallback? onClose; + final void Function()? onClose; final Future Function()? canClose; final bool asBarrier; - /// 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; - - 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({ + Key? 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.onClose, + this.canClose, + this.asBarrier = false, + }) : super(key: key); + @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(); + super.initState(); + } + + 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(); + }, + onExit: () => _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(), + ), + ); + + return Stack(children: children); + }); + _rootEntry.addEntry(context, this, newEntry, widget.asBarrier); + } + + void close() { + if (_rootEntry.contains(this)) { + _rootEntry.removeEntry(this); + widget.onClose?.call(); + } + } + + void _removeRootOverlay() { + _rootEntry.popEntry(); + + if (widget.mutex?.state == this) { + widget.mutex?.removeState(); + } } @override void deactivate() { - close(notify: false); - + close(); super.deactivate(); } - @override - void dispose() { - isDisposed = true; - animationController.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return PopoverTarget( @@ -204,323 +181,48 @@ 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; - } - - showOverlay(cursorPosition); - }, - ); - - 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); - } - }, - 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); - } - }, - child: child, - ), - }; - } - - void _callHandler(VoidCallback handler) { - 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(); + return MouseRegion( + onEnter: (event) { + if (widget.triggerActions & PopoverTriggerFlags.hover != 0) { + showOverlay(); } }, - ); - } - - 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, + child: Listener( + child: widget.child, + onPointerDown: (PointerDownEvent event) { + if (widget.triggerActions & PopoverTriggerFlags.click != 0) { + showOverlay(); + } + }, ), ); } - - 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 { - const PopoverContainer({ - super.key, - required this.popupBuilder, - required this.delegate, - required this.onClose, - required this.onCloseAll, - required this.skipTraversal, - }); - 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; - final PopoverLayoutDelegate delegate; + + const PopoverContainer({ + Key? key, + required this.popupBuilder, + required this.direction, + required this.popoverLink, + required this.offset, + required this.windowPadding, + required this.onClose, + required this.onCloseAll, + }) : super(key: key); @override State createState() => PopoverContainerState(); @@ -529,27 +231,23 @@ class PopoverContainer extends StatefulWidget { if (context is StatefulElement && context.state is PopoverContainerState) { return context.state as PopoverContainerState; } - return context.findAncestorStateOfType()!; - } - - static PopoverContainerState? maybeOf(BuildContext context) { - if (context is StatefulElement && context.state is PopoverContainerState) { - return context.state as PopoverContainerState; - } - return context.findAncestorStateOfType(); + final PopoverContainerState? result = + context.findAncestorStateOfType(); + return result!; } } class PopoverContainerState extends State { @override Widget build(BuildContext context) { - return Focus( - autofocus: true, - skipTraversal: widget.skipTraversal, - child: CustomSingleChildLayout( - delegate: widget.delegate, - child: widget.popupBuilder(context), + return CustomSingleChildLayout( + 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..137faaccae 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: ^2.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_result/.gitignore b/frontend/appflowy_flutter/packages/appflowy_result/.gitignore deleted file mode 100644 index ac5aa9893e..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/.gitignore +++ /dev/null @@ -1,29 +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/ -build/ diff --git a/frontend/appflowy_flutter/packages/appflowy_result/.metadata b/frontend/appflowy_flutter/packages/appflowy_result/.metadata deleted file mode 100644 index ea18fe993d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/.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: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4" - channel: "[user-branch]" - -project_type: package diff --git a/frontend/appflowy_flutter/packages/appflowy_result/CHANGELOG.md b/frontend/appflowy_flutter/packages/appflowy_result/CHANGELOG.md deleted file mode 100644 index 41cc7d8192..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/frontend/appflowy_flutter/packages/appflowy_result/LICENSE b/frontend/appflowy_flutter/packages/appflowy_result/LICENSE deleted file mode 100644 index ba75c69f7f..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/frontend/appflowy_flutter/packages/appflowy_result/README.md b/frontend/appflowy_flutter/packages/appflowy_result/README.md deleted file mode 100644 index 02fe8ecabc..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/README.md +++ /dev/null @@ -1,39 +0,0 @@ - - -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. - -## Features - -TODO: List what your package can do. Maybe include images, gifs, or videos. - -## Getting started - -TODO: List prerequisites and provide or point to information on how to -start using the package. - -## Usage - -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. - -```dart -const like = 'sample'; -``` - -## Additional information - -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. diff --git a/frontend/appflowy_flutter/packages/appflowy_result/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_result/analysis_options.yaml deleted file mode 100644 index a5744c1cfb..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart deleted file mode 100644 index d91c9e4954..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// AppFlowyPopover library -library; - -export 'src/async_result.dart'; -export 'src/result.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart deleted file mode 100644 index 94cd9a68a6..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:appflowy_result/appflowy_result.dart'; - -typedef FlowyAsyncResult = Future>; - -extension FlowyAsyncResultExtension - on FlowyAsyncResult { - Future getOrElse(S Function(F f) onFailure) { - return then((result) => result.getOrElse(onFailure)); - } - - Future toNullable() { - return then((result) => result.toNullable()); - } - - Future getOrThrow() { - return then((result) => result.getOrThrow()); - } - - Future fold( - W Function(S s) onSuccess, - W Function(F f) onFailure, - ) { - return then((result) => result.fold(onSuccess, onFailure)); - } - - Future isError() { - return then((result) => result.isFailure); - } - - Future isSuccess() { - return then((result) => result.isSuccess); - } - - FlowyAsyncResult onFailure(void Function(F failure) onFailure) { - return then((result) => result..onFailure(onFailure)); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart deleted file mode 100644 index e8d3be8d90..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart +++ /dev/null @@ -1,167 +0,0 @@ -abstract class FlowyResult { - const FlowyResult(); - - factory FlowyResult.success(S s) => FlowySuccess(s); - - factory FlowyResult.failure(F f) => FlowyFailure(f); - - T fold(T Function(S s) onSuccess, T Function(F f) onFailure); - - FlowyResult map(T Function(S success) fn); - FlowyResult mapError(T Function(F failure) fn); - - bool get isSuccess; - bool get isFailure; - - S? toNullable(); - - T? onSuccess(T? Function(S s) onSuccess); - T? onFailure(T? Function(F f) onFailure); - - S getOrElse(S Function(F failure) onFailure); - S getOrThrow(); - - F getFailure(); -} - -class FlowySuccess implements FlowyResult { - final S _value; - - FlowySuccess(this._value); - - S get value => _value; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FlowySuccess && - runtimeType == other.runtimeType && - _value == other._value; - - @override - int get hashCode => _value.hashCode; - - @override - String toString() => 'Success(value: $_value)'; - - @override - T fold(T Function(S s) onSuccess, T Function(F e) onFailure) => - onSuccess(_value); - - @override - map(T Function(S success) fn) { - return FlowySuccess(fn(_value)); - } - - @override - FlowyResult mapError(T Function(F error) fn) { - return FlowySuccess(_value); - } - - @override - bool get isSuccess => true; - - @override - bool get isFailure => false; - - @override - S? toNullable() { - return _value; - } - - @override - T? onSuccess(T? Function(S success) onSuccess) { - return onSuccess(_value); - } - - @override - T? onFailure(T? Function(F failure) onFailure) { - return null; - } - - @override - S getOrElse(S Function(F failure) onFailure) { - return _value; - } - - @override - S getOrThrow() { - return _value; - } - - @override - F getFailure() { - throw UnimplementedError(); - } -} - -class FlowyFailure implements FlowyResult { - final F _value; - - FlowyFailure(this._value); - - F get error => _value; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FlowyFailure && - runtimeType == other.runtimeType && - _value == other._value; - - @override - int get hashCode => _value.hashCode; - - @override - String toString() => 'Failure(error: $_value)'; - - @override - T fold(T Function(S s) onSuccess, T Function(F e) onFailure) => - onFailure(_value); - - @override - map(T Function(S success) fn) { - return FlowyFailure(_value); - } - - @override - FlowyResult mapError(T Function(F error) fn) { - return FlowyFailure(fn(_value)); - } - - @override - bool get isSuccess => false; - - @override - bool get isFailure => true; - - @override - S? toNullable() { - return null; - } - - @override - T? onSuccess(T? Function(S success) onSuccess) { - return null; - } - - @override - T? onFailure(T? Function(F failure) onFailure) { - return onFailure(_value); - } - - @override - S getOrElse(S Function(F failure) onFailure) { - return onFailure(_value); - } - - @override - S getOrThrow() { - throw _value; - } - - @override - F getFailure() { - return _value; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml deleted file mode 100644 index 5d8f0d88c2..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: appflowy_result -description: "A new Flutter package project." -version: 0.0.1 -homepage: https://github.com/appflowy-io/appflowy - -environment: - sdk: ">=3.3.0 <4.0.0" - flutter: ">=1.17.0" - -dev_dependencies: - flutter_lints: ^3.0.0 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 584d50c07b..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'button/button.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/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 26e45ca8f1..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: data!.evaluate(animation), - 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 fe774d3561..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ /dev/null @@ -1,326 +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() { - final textStyle = AppFlowyBaseTextStyle(); - 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( - 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() { - final textStyle = AppFlowyBaseTextStyle(); - 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( - 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 6ef43076c5..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart +++ /dev/null @@ -1,23 +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() { - // TODO: implement light - throw UnimplementedError(); - } - - @override - AppFlowyThemeData dark() { - // TODO: implement dark - 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 28eee5b145..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBorderColorScheme { - const AppFlowyBorderColorScheme({ - 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 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( - 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 3cdf267fe0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart +++ /dev/null @@ -1,517 +0,0 @@ -import 'package:flutter/widgets.dart'; - -abstract class TextThemeType { - const TextThemeType(); - - 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(); - - @override - TextStyle standard({ - String family = '', - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({ - String family = '', - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({ - String family = '', - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({ - String family = '', - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family, - 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(); - - @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - 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(); - - @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - 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(); - - @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - 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(); - - @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - 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(); - - @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - 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(); - - @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - 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(); - - @override - TextStyle standard({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String family = '', Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family, - 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 d96ca0f557..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:appflowy_ui/src/theme/definition/text_style/base/default_text_style.dart'; - -class AppFlowyBaseTextStyle { - const AppFlowyBaseTextStyle({ - this.heading1 = const TextThemeHeading1(), - this.heading2 = const TextThemeHeading2(), - this.heading3 = const TextThemeHeading3(), - this.heading4 = const TextThemeHeading4(), - this.headline = const TextThemeHeadline(), - this.title = const TextThemeTitle(), - this.body = const TextThemeBody(), - this.caption = const TextThemeCaption(), - }); - - 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 515e6b2ecf..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart +++ /dev/null @@ -1,86 +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(); - AppFlowyThemeData dark(); -} 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..22bf6a93ca 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -1,19 +1,15 @@ -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'; +import 'package:flowy_infra/theme.dart'; import 'default_colorscheme.dart'; +import 'dandelion.dart'; import 'lavender.dart'; -import 'lemonade.dart'; - -part 'colorscheme.g.dart'; /// A map of all the built-in themes. /// /// The key is the theme name, and the value is a list of two color schemes: /// the first is for light mode, and the second is for dark mode. + const Map> themeMap = { BuiltInTheme.defaultTheme: [ DefaultColorScheme.light(), @@ -23,18 +19,65 @@ const Map> themeMap = { DandelionColorScheme.light(), DandelionColorScheme.dark(), ], - BuiltInTheme.lemonade: [ - LemonadeColorScheme.light(), - LemonadeColorScheme.dark(), - ], BuiltInTheme.lavender: [ LavenderColorScheme.light(), LavenderColorScheme.dark(), ], }; -@JsonSerializable(converters: [ColorConverter()]) -class FlowyColorScheme { +@immutable +abstract class FlowyColorScheme { + final Color surface; + final Color hover; + final Color selector; + final Color red; + final Color yellow; + final Color green; + final Color shader1; + final Color shader2; + final Color shader3; + final Color shader4; + final Color shader5; + final Color shader6; + final Color shader7; + final Color bg1; + final Color bg2; + final Color bg3; + final Color bg4; + final Color tint1; + final Color tint2; + final Color tint3; + final Color tint4; + final Color tint5; + final Color tint6; + final Color tint7; + final Color tint8; + final Color tint9; + final Color main1; + final Color main2; + final Color shadow; + final Color sidebarBg; + final Color divider; + final Color topbarBg; + final Color icon; + final Color text; + final Color input; + final Color hint; + final Color primary; + final Color onPrimary; + //page title hover effect + final Color hoverBG1; + //action item hover effect + final Color hoverBG2; + final Color hoverBG3; + //the text color when it is hovered + final Color hoverFG; + final Color questionBubbleBG; + final Color progressBarBGColor; + //editor toolbar BG color + final Color toolbarColor; + final Color toggleButtonBGColor; + const FlowyColorScheme({ required this.surface, required this.hover, @@ -70,8 +113,6 @@ class FlowyColorScheme { required this.topbarBg, required this.icon, required this.text, - required this.secondaryText, - required this.strongText, required this.input, required this.hint, required this.primary, @@ -84,116 +125,14 @@ class FlowyColorScheme { required this.progressBarBGColor, required this.toolbarColor, 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; - final Color hover; - final Color selector; - final Color red; - final Color yellow; - final Color green; - final Color shader1; - final Color shader2; - final Color shader3; - final Color shader4; - final Color shader5; - final Color shader6; - final Color shader7; - final Color bg1; - final Color bg2; - final Color bg3; - final Color bg4; - final Color tint1; - final Color tint2; - final Color tint3; - final Color tint4; - final Color tint5; - final Color tint6; - final Color tint7; - final Color tint8; - final Color tint9; - final Color main1; - final Color main2; - final Color shadow; - final Color sidebarBg; - final Color divider; - final Color topbarBg; - final Color icon; - final Color text; - final Color secondaryText; - final Color strongText; - final Color input; - final Color hint; - final Color primary; - final Color onPrimary; - //page title hover effect - final Color hoverBG1; - //action item hover effect - final Color hoverBG2; - final Color hoverBG3; - //the text color when it is hovered - final Color hoverFG; - final Color questionBubbleBG; - final Color progressBarBGColor; - //editor toolbar BG color - final Color toolbarColor; - final Color toggleButtonBGColor; - final Color calendarWeekendBGColor; - //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(); + factory FlowyColorScheme.builtIn(String themeName, Brightness brightness) { + switch (brightness) { + case Brightness.light: + return themeMap[themeName]?[0] ?? const DefaultColorScheme.light(); + case Brightness.dark: + return themeMap[themeName]?[1] ?? const DefaultColorScheme.dark(); + } } } 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..ae041525b2 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -1,18 +1,18 @@ -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; import 'package:flutter/material.dart'; import 'colorscheme.dart'; const _black = Color(0xff000000); const _white = Color(0xFFFFFFFF); +const _lightHover = Color(0xFFe0f8ff); +const _lightSelector = Color(0xfff2fcff); const _lightBg1 = Color(0xFFFFD13E); +const _lightBg2 = Color(0xffedeef2); const _lightShader1 = Color(0xff333333); const _lightShader3 = Color(0xff828282); const _lightShader5 = Color(0xffe0e0e0); const _lightShader6 = Color(0xfff2f2f2); -const _lightDandelionYellow = Color(0xffffcb00); -const _lightDandelionLightYellow = Color(0xffffdf66); -const _lightDandelionGreen = Color(0xff9bc53d); +const _lightMain1 = Color(0xffe21f74); const _lightTint9 = Color(0xffe1fbff); const _darkShader1 = Color(0xff131720); @@ -20,7 +20,7 @@ const _darkShader2 = Color(0xff1A202C); const _darkShader3 = Color(0xff363D49); const _darkShader5 = Color(0xffBBC3CD); const _darkShader6 = Color(0xffF2F2F2); -const _darkMain1 = Color(0xffffcb00); +const _darkMain1 = Color(0xffe21f74); const _darkInput = Color(0xff282E3A); class DandelionColorScheme extends FlowyColorScheme { @@ -28,23 +28,20 @@ class DandelionColorScheme extends FlowyColorScheme { : super( surface: Colors.white, hover: const Color(0xFFe0f8ff), - // hover effect on setting value - selector: _lightDandelionLightYellow, + selector: const Color(0xfff2fcff), red: const Color(0xfffb006d), yellow: const Color(0xffffd667), green: const Color(0xff66cf80), shader1: const Color(0xff333333), shader2: const Color(0xff4f4f4f), shader3: const Color(0xff828282), - // disable text color shader4: const Color(0xffbdbdbd), shader5: _lightShader5, shader6: const Color(0xfff2f2f2), shader7: _black, bg1: _lightBg1, bg2: const Color(0xffedeef2), - // Hover color on trash button - bg3: _lightDandelionYellow, + bg3: const Color(0xffe2e4eb), bg4: const Color(0xff2c144b), tint1: const Color(0xffe8e0ff), tint2: const Color(0xffffe7fd), @@ -55,45 +52,33 @@ class DandelionColorScheme extends FlowyColorScheme { tint7: const Color(0xffddffd6), tint8: const Color(0xffdefff1), tint9: _lightTint9, - main1: _lightDandelionYellow, - // cursor color - main2: _lightDandelionYellow, - shadow: const Color.fromRGBO(0, 0, 0, 0.15), - sidebarBg: _lightDandelionGreen, + main1: _lightMain1, + main2: const Color(0xffe0196f), + shadow: _black, + sidebarBg: _lightBg1, divider: _lightShader6, topbarBg: _white, icon: _lightShader1, text: _lightShader1, - secondaryText: _lightShader1, - strongText: Colors.black, input: _white, hint: _lightShader3, - primary: _lightDandelionYellow, - onPrimary: _lightShader1, - // hover color in sidebar - hoverBG1: _lightDandelionYellow, - // tool bar hover color - hoverBG2: _lightDandelionLightYellow, + primary: _lightMain1, + onPrimary: _white, + hoverBG1: _lightBg2, + hoverBG2: _lightHover, hoverBG3: _lightShader6, hoverFG: _lightShader1, - questionBubbleBG: _lightDandelionLightYellow, + questionBubbleBG: _lightSelector, progressBarBGColor: _lightTint9, toolbarColor: _lightShader1, - 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), + toggleButtonBGColor: _lightShader5, ); const DandelionColorScheme.dark() : super( surface: const Color(0xff292929), hover: const Color(0xff1f1f1f), - selector: _darkShader2, + selector: const Color(0xff333333), red: const Color(0xfffb006d), yellow: const Color(0xffffd667), green: const Color(0xff66cf80), @@ -106,27 +91,25 @@ class DandelionColorScheme extends FlowyColorScheme { shader7: _white, bg1: const Color(0xFFD5A200), bg2: _black, - bg3: _darkMain1, + bg3: const Color(0xff4f4f4f), 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), + tint1: const Color(0xff8738F5), + tint2: const Color(0xffE6336E), + tint3: const Color(0xffFF2D9E), + tint4: const Color(0xffE9973E), + tint5: const Color(0xffFBF000), + tint6: const Color(0xffC0F000), + tint7: const Color(0xff15F74E), + tint8: const Color(0xff00F0E2), + tint9: const Color(0xff00BCF0), main1: _darkMain1, - main2: _darkMain1, - shadow: const Color(0xff0F131C), - sidebarBg: const Color(0xff25300e), + main2: const Color(0xffe0196f), + 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, @@ -139,12 +122,5 @@ class DandelionColorScheme extends FlowyColorScheme { 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, ); } 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..b3f324504e 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,146 +2,123 @@ 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(0xffedeef2); +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 _darkInput = Color(0xff282E3A); class DefaultColorScheme extends FlowyColorScheme { const DefaultColorScheme.light() : super( - surface: ColorSchemeConstants.white, - hover: ColorSchemeConstants.lightHover, - selector: ColorSchemeConstants.lightSelector, - red: const Color(0xFFfb006d), - yellow: const Color(0xFFFFd667), - green: const Color(0xFF66cf80), - shader1: ColorSchemeConstants.lightShader1, - shader2: const Color(0xFF4f4f4f), - shader3: ColorSchemeConstants.lightShader3, - shader4: const Color(0xFFbdbdbd), - shader5: ColorSchemeConstants.lightShader5, - shader6: ColorSchemeConstants.lightShader6, - shader7: ColorSchemeConstants.lightShader1, - bg1: ColorSchemeConstants.lightBg1, - bg2: ColorSchemeConstants.lightBg2, - bg3: const Color(0xFFe2e4eb), - bg4: const Color(0xFF2c144b), - tint1: const Color(0xFFe8e0FF), - tint2: const Color(0xFFFFe7fd), - tint3: const Color(0xFFFFe7ee), - tint4: const Color(0xFFFFefe3), - tint5: const Color(0xFFFFf2cd), - tint6: const Color(0xFFf5FFdc), - tint7: const Color(0xFFddFFd6), - tint8: const Color(0xFFdeFFf1), - tint9: ColorSchemeConstants.lightTint9, - main1: ColorSchemeConstants.lightMain1, - main2: const Color(0xFF00b7ea), + surface: _white, + hover: _lightHover, + selector: _lightSelector, + red: const Color(0xfffb006d), + yellow: const Color(0xffffd667), + green: const Color(0xff66cf80), + shader1: _lightShader1, + shader2: const Color(0xff4f4f4f), + shader3: _lightShader3, + shader4: const Color(0xffbdbdbd), + shader5: _lightShader5, + shader6: _lightShader6, + shader7: _lightShader1, + bg1: _lightBg1, + bg2: _lightBg2, + bg3: const Color(0xffe2e4eb), + bg4: const Color(0xff2c144b), + tint1: const Color(0xffe8e0ff), + tint2: const Color(0xffffe7fd), + tint3: const Color(0xffffe7ee), + tint4: const Color(0xffffefe3), + tint5: const Color(0xfffff2cd), + tint6: const Color(0xfff5ffdc), + tint7: const Color(0xffddffd6), + tint8: const Color(0xffdefff1), + 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, - 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, - 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), + sidebarBg: _lightBg1, + divider: _lightShader6, + topbarBg: _white, + icon: _lightShader1, + text: _lightShader1, + input: _white, + hint: _lightShader3, + primary: _lightMain1, + onPrimary: _white, + hoverBG1: _lightBg2, + hoverBG2: _lightHover, + hoverFG: _lightShader1, + questionBubbleBG: _lightSelector, + hoverBG3: _lightShader6, + progressBarBGColor: _lightTint9, + toolbarColor: _lightShader1, + toggleButtonBGColor: _lightShader5, ); const DefaultColorScheme.dark() : super( - surface: ColorSchemeConstants.darkShader2, - hover: ColorSchemeConstants.darkMain1, - selector: ColorSchemeConstants.darkShader2, - red: const Color(0xFFfb006d), - yellow: const Color(0xFFF7CF46), - green: const Color(0xFF66CF80), - shader1: ColorSchemeConstants.darkShader1, - shader2: ColorSchemeConstants.darkShader2, - shader3: ColorSchemeConstants.darkShader3, - shader4: const Color(0xFF505469), - shader5: ColorSchemeConstants.darkShader5, - shader6: ColorSchemeConstants.darkShader6, - shader7: ColorSchemeConstants.white, - bg1: const Color(0xFF1A202C), - bg2: const Color(0xFFEDEEF2), - bg3: ColorSchemeConstants.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, - 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, - strongText: Colors.white, - input: ColorSchemeConstants.darkInput, - hint: const Color(0xFF59647a), - primary: ColorSchemeConstants.darkMain2, - onPrimary: ColorSchemeConstants.darkShader1, - hoverBG1: const Color(0x1AFFFFFF), - hoverBG2: ColorSchemeConstants.darkMain1, - hoverBG3: ColorSchemeConstants.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, + surface: _darkShader2, + hover: _darkMain1, + selector: _darkShader2, + red: const Color(0xfffb006d), + yellow: const Color(0xffF7CF46), + green: const Color(0xff66CF80), + shader1: _darkShader1, + shader2: _darkShader2, + shader3: _darkShader3, + shader4: const Color(0xff7C8CA5), + shader5: _darkShader5, + shader6: _darkShader6, + shader7: _white, + bg1: const Color(0xffF7F8FC), + bg2: const Color(0xffEDEEF2), + bg3: _darkMain1, + bg4: const Color(0xff2C144B), + tint1: const Color(0xff8738F5), + tint2: const Color(0xffE6336E), + tint3: const Color(0xffFF2D9E), + tint4: const Color(0xffE9973E), + tint5: const Color(0xffFBF000), + tint6: const Color(0xffC0F000), + tint7: const Color(0xff15F74E), + tint8: const Color(0xff00F0E2), + tint9: const Color(0xff00BCF0), + main1: _darkMain1, + main2: const Color(0xff00B7EA), + shadow: const Color(0xff0F131C), + sidebarBg: const Color(0xff232B38), + divider: _darkShader3, + topbarBg: _darkShader1, + icon: _darkShader5, + text: _darkShader5, + input: _darkInput, + hint: _darkShader5, + primary: _darkMain1, + onPrimary: _darkShader1, + hoverBG1: _darkMain1, + hoverBG2: _darkMain1, + hoverBG3: _darkShader3, + hoverFG: _darkShader1, + questionBubbleBG: _darkShader3, + progressBarBGColor: _darkShader3, + toolbarColor: _darkInput, + toggleButtonBGColor: _darkShader1, ); } 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..e35ee623a6 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'; @@ -6,15 +5,15 @@ import 'colorscheme.dart'; const _black = Color(0xff000000); const _white = Color(0xFFFFFFFF); -const _lightHover = Color(0xffd8d6fc); -const _lightSelector = Color(0xffe5e3f9); -const _lightBg1 = Color(0xfff2f0f6); -const _lightBg2 = Color(0xffd8d6fc); +const _lightHover = Color(0xFFe0f8ff); +const _lightSelector = Color(0xfff2fcff); +const _lightBg1 = Color(0xfff7f8fc); +const _lightBg2 = Color(0xffedeef2); const _lightShader1 = Color(0xff333333); const _lightShader3 = Color(0xff828282); const _lightShader5 = Color(0xffe0e0e0); -const _lightShader6 = Color(0xffd8d6fc); -const _lightMain1 = Color(0xffaba9e7); +const _lightShader6 = Color(0xfff2f2f2); +const _lightMain1 = Color(0xffA652FB); const _lightTint9 = Color(0xffe1fbff); const _darkShader1 = Color(0xff131720); @@ -22,15 +21,15 @@ const _darkShader2 = Color(0xff1A202C); const _darkShader3 = Color(0xff363D49); const _darkShader5 = Color(0xffBBC3CD); const _darkShader6 = Color(0xffF2F2F2); -const _darkMain1 = Color(0xffab00ff); +const _darkMain1 = Color(0xffA652FB); const _darkInput = Color(0xff282E3A); class LavenderColorScheme extends FlowyColorScheme { const LavenderColorScheme.light() : super( surface: Colors.white, - hover: _lightHover, - selector: _lightSelector, + hover: const Color(0xFFe0f8ff), + selector: const Color(0xfff2fcff), red: const Color(0xfffb006d), yellow: const Color(0xffffd667), green: const Color(0xff66cf80), @@ -43,7 +42,7 @@ class LavenderColorScheme extends FlowyColorScheme { shader7: _black, bg1: const Color(0xffAC59FF), bg2: const Color(0xffedeef2), - bg3: _lightHover, + bg3: const Color(0xffe2e4eb), bg4: const Color(0xff2c144b), tint1: const Color(0xffe8e0ff), tint2: const Color(0xffffe7fd), @@ -53,21 +52,19 @@ class LavenderColorScheme extends FlowyColorScheme { tint6: const Color(0xfff5ffdc), tint7: const Color(0xffddffd6), tint8: const Color(0xffdefff1), - tint9: _lightMain1, + tint9: _lightTint9, main1: _lightMain1, - main2: _lightMain1, - shadow: const Color.fromRGBO(0, 0, 0, 0.15), + main2: const Color(0xff9327FF), + shadow: _black, sidebarBg: _lightBg1, divider: _lightShader6, topbarBg: _white, icon: _lightShader1, text: _lightShader1, - secondaryText: _lightShader1, - strongText: Colors.black, input: _white, hint: _lightShader3, primary: _lightMain1, - onPrimary: _lightShader1, + onPrimary: _white, hoverBG1: _lightBg2, hoverBG2: _lightHover, hoverBG3: _lightShader6, @@ -75,21 +72,14 @@ class LavenderColorScheme extends FlowyColorScheme { questionBubbleBG: _lightSelector, progressBarBGColor: _lightTint9, toolbarColor: _lightShader1, - 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), + toggleButtonBGColor: _lightShader5, ); const LavenderColorScheme.dark() : super( surface: const Color(0xFF1B1A1D), - hover: _darkMain1, - selector: _darkShader2, + hover: const Color(0xff1f1f1f), + selector: const Color(0xff333333), red: const Color(0xfffb006d), yellow: const Color(0xffffd667), green: const Color(0xff66cf80), @@ -102,28 +92,26 @@ class LavenderColorScheme extends FlowyColorScheme { shader7: _white, bg1: const Color(0xff8C23F6), bg2: _black, - bg3: _darkMain1, + bg3: const Color(0xff4f4f4f), 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), + tint1: const Color(0xff8738F5), + tint2: const Color(0xffE6336E), + tint3: const Color(0xffFF2D9E), + tint4: const Color(0xffE9973E), + tint5: const Color(0xffFBF000), + tint6: const Color(0xffC0F000), + tint7: const Color(0xff15F74E), + tint8: const Color(0xff00F0E2), + tint9: const Color(0xff00BCF0), main1: _darkMain1, - main2: _darkMain1, - shadow: const Color(0xff0F131C), - sidebarBg: const Color(0xff2D223B), + main2: const Color(0xff9327FF), + shadow: _black, + sidebarBg: const Color(0xff232B38), divider: _darkShader3, topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, - secondaryText: _darkShader5, - strongText: Colors.white, - input: _darkInput, + input: const Color(0xff282E3A), hint: _darkShader5, primary: _darkMain1, onPrimary: _darkShader1, @@ -135,12 +123,5 @@ class LavenderColorScheme extends FlowyColorScheme { 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, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart deleted file mode 100644 index 3f39ae4c84..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; -import 'package:flutter/material.dart'; - -import 'colorscheme.dart'; - -const _black = Color(0xff000000); -const _white = Color(0xFFFFFFFF); -const _lightBg1 = Color(0xFFFFD13E); -const _lightShader1 = Color(0xff333333); -const _lightShader3 = Color(0xff828282); -const _lightShader5 = Color(0xffe0e0e0); -const _lightShader6 = Color(0xfff2f2f2); -const _lightDandelionYellow = Color(0xffffcb00); -const _lightDandelionLightYellow = Color(0xffffdf66); -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(0xffffcb00); -const _darkInput = Color(0xff282E3A); - -// Derive from [DandelionColorScheme] -// Use a light yellow color in the sidebar intead of a green color in Dandelion -// Some field name are still included 'Dandelion' to indicate they are the same color as the one in Dandelion -class LemonadeColorScheme extends FlowyColorScheme { - const LemonadeColorScheme.light() - : super( - surface: Colors.white, - hover: const Color(0xFFe0f8ff), - // hover effect on setting value - selector: _lightDandelionLightYellow, - red: const Color(0xfffb006d), - yellow: const Color(0xffffd667), - green: const Color(0xff66cf80), - shader1: const Color(0xff333333), - shader2: const Color(0xff4f4f4f), - shader3: const Color(0xff828282), - // disable text color - shader4: const Color(0xffbdbdbd), - shader5: _lightShader5, - shader6: const Color(0xfff2f2f2), - shader7: _black, - bg1: _lightBg1, - bg2: const Color(0xffedeef2), - // Hover color on trash button - bg3: _lightDandelionYellow, - bg4: const Color(0xff2c144b), - tint1: const Color(0xffe8e0ff), - tint2: const Color(0xffffe7fd), - tint3: const Color(0xffffe7ee), - tint4: const Color(0xffffefe3), - tint5: const Color(0xfffff2cd), - tint6: const Color(0xfff5ffdc), - tint7: const Color(0xffddffd6), - tint8: const Color(0xffdefff1), - tint9: _lightTint9, - main1: _lightDandelionYellow, - // cursor color - main2: _lightDandelionYellow, - shadow: const Color.fromRGBO(0, 0, 0, 0.15), - sidebarBg: const Color(0xfffaf0c8), - divider: _lightShader6, - topbarBg: _white, - icon: _lightShader1, - text: _lightShader1, - secondaryText: _lightShader1, - strongText: Colors.black, - input: _white, - hint: _lightShader3, - primary: _lightDandelionYellow, - onPrimary: _lightShader1, - // hover color in sidebar - hoverBG1: _lightDandelionYellow, - // tool bar hover color - hoverBG2: _lightDandelionLightYellow, - hoverBG3: _lightShader6, - hoverFG: _lightShader1, - questionBubbleBG: _lightDandelionLightYellow, - progressBarBGColor: _lightTint9, - toolbarColor: _lightShader1, - 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); -} 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 deleted file mode 100644 index 1e6f6a99e2..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/services.dart'; - -import 'package:file_picker/file_picker.dart' as fp; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; - -class FilePicker implements FilePickerService { - @override - Future getDirectoryPath({String? title}) { - return fp.FilePicker.platform.getDirectoryPath(); - } - - @override - Future pickFiles({ - String? dialogTitle, - String? initialDirectory, - fp.FileType type = fp.FileType.any, - List? allowedExtensions, - Function(fp.FilePickerStatus p1)? onFileLoading, - bool allowCompression = true, - bool allowMultiple = false, - bool withData = false, - bool withReadStream = false, - bool lockParentWindow = false, - }) async { - final result = await fp.FilePicker.platform.pickFiles( - dialogTitle: dialogTitle, - initialDirectory: initialDirectory, - type: type, - allowedExtensions: allowedExtensions, - onFileLoading: onFileLoading, - allowCompression: allowCompression, - allowMultiple: allowMultiple, - withData: withData, - withReadStream: withReadStream, - lockParentWindow: lockParentWindow, - ); - 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, - String? fileName, - String? initialDirectory, - FileType type = FileType.any, - List? allowedExtensions, - bool lockParentWindow = false, - Uint8List? bytes, - }) async { - final result = await fp.FilePicker.platform.saveFile( - dialogTitle: dialogTitle, - fileName: fileName, - initialDirectory: initialDirectory, - type: type, - allowedExtensions: allowedExtensions, - lockParentWindow: lockParentWindow, - bytes: bytes, - ); - - return result; - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart index c797d4c513..e6df4c7428 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart @@ -13,7 +13,6 @@ /// /// /// -library; // ignore_for_file: constant_identifier_names import 'package:flutter/widgets.dart'; diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/image.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/image.dart new file mode 100644 index 0000000000..6a0c3b8d3b --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/image.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +/// For icon that needs to change color when it is on hovered +/// +/// Get the hover color from ThemeData +class FlowySvg extends StatelessWidget { + const FlowySvg({super.key, this.size, required this.name}); + final String name; + final Size? size; + + @override + Widget build(BuildContext context) { + return svgWidget( + name, + size: size, + color: Theme.of(context).iconTheme.color, + ); + } +} + +Widget svgWidget(String name, {Size? size, Color? color}) { + if (size != null) { + return SizedBox.fromSize( + size: size, + child: SvgPicture.asset( + 'assets/images/$name.svg', + colorFilter: + color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null, + ), + ); + } else { + return SvgPicture.asset( + 'assets/images/$name.svg', + colorFilter: + color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index 6f37058f00..644957e82d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -16,29 +16,16 @@ String languageFromLocale(Locale locale) { } // Then in alphabetical order - case "am": - return "አማርኛ"; case "ar": return "العربية"; case "ca": return "Català"; - case "cs": - return "Čeština"; - case "ckb": - switch (locale.countryCode) { - case "KU": - return "کوردی سۆرانی"; - default: - return locale.languageCode; - } case "de": return "Deutsch"; case "es": return "Español"; case "eu": return "Euskera"; - case "el": - return "Ελληνικά"; case "fr": switch (locale.countryCode) { case "CA": @@ -48,10 +35,6 @@ String languageFromLocale(Locale locale) { default: return locale.languageCode; } - case "mr": - return "मराठी"; - case "he": - return "עברית"; case "hu": return "Magyar"; case "id": @@ -70,19 +53,11 @@ String languageFromLocale(Locale locale) { return "русский"; case "sv": return "Svenska"; - case "th": - return "ไทย"; case "tr": return "Türkçe"; - case "fa": - return "فارسی"; - case "uk": - return "українська"; - case "ur": - return "اردو"; - case "hin": - return "हिन्दी"; + + // If not found then the language code will be displayed + default: + return locale.languageCode; } - // If not found then the language code will be displayed - return locale.languageCode; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart index 41017faafb..bbb55bf885 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart @@ -48,3 +48,9 @@ class PublishNotifier extends ChangeNotifier { ); } } + +class Notifier extends ChangeNotifier { + void notify() { + notifyListeners(); + } +} 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 deleted file mode 100644 index 0dbfc84564..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:flowy_infra/plugins/service/models/exceptions.dart'; -import 'package:flowy_infra/plugins/service/plugin_service.dart'; - -import '../../file_picker/file_picker_impl.dart'; - -import 'dynamic_plugin_event.dart'; -import 'dynamic_plugin_state.dart'; - -class DynamicPluginBloc extends Bloc { - DynamicPluginBloc({FilePicker? filePicker}) - : super(const DynamicPluginState.uninitialized()) { - on(dispatch); - add(DynamicPluginEvent.load()); - } - - Future dispatch( - DynamicPluginEvent event, Emitter emit) async { - await event.when( - addPlugin: () => addPlugin(emit), - removePlugin: (name) => removePlugin(emit, name), - load: () => onLoadRequested(emit), - ); - } - - Future onLoadRequested(Emitter emit) async { - emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); - } - - Future addPlugin(Emitter emit) async { - emit(const DynamicPluginState.processing()); - try { - final plugin = await FlowyPluginService.pick(); - if (plugin == null) { - return emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); - } - await FlowyPluginService.instance.addPlugin(plugin); - } on PluginCompilationException catch (exception) { - return emit( - DynamicPluginState.compilationFailure(errorMessage: exception.message), - ); - } - - emit(const DynamicPluginState.compilationSuccess()); - emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); - } - - Future removePlugin( - 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, - ), - ); - } - - await FlowyPluginService.removePlugin(plugin); - - emit(const DynamicPluginState.deletionSuccess()); - emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_event.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_event.dart deleted file mode 100644 index 71c1fce9c0..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_event.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'dynamic_plugin_event.freezed.dart'; - -@freezed -class DynamicPluginEvent with _$DynamicPluginEvent { - factory DynamicPluginEvent.addPlugin() = _AddPlugin; - factory DynamicPluginEvent.removePlugin({required String name}) = - _RemovePlugin; - factory DynamicPluginEvent.load() = _Load; -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_state.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_state.dart deleted file mode 100644 index cc34e8ebac..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_state.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../service/models/flowy_dynamic_plugin.dart'; - -part 'dynamic_plugin_state.freezed.dart'; - -@freezed -class DynamicPluginState with _$DynamicPluginState { - const factory DynamicPluginState.uninitialized() = _Uninitialized; - const factory DynamicPluginState.ready({ - required Iterable plugins, - }) = Ready; - const factory DynamicPluginState.processing() = _Processing; - const factory DynamicPluginState.compilationFailure( - {required String errorMessage}) = _CompilationFailure; - const factory DynamicPluginState.deletionFailure({ - required String path, - }) = _DeletionFailure; - const factory DynamicPluginState.deletionSuccess() = _DeletionSuccess; - const factory DynamicPluginState.compilationSuccess() = _CompilationSuccess; -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart deleted file mode 100644 index 64c6d2b586..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'dart:io'; - -class PluginLocationService { - const PluginLocationService({ - required Future fallback, - }) : _fallback = fallback; - - final Future _fallback; - - Future get fallback async => _fallback; - - Future get location async => fallback; -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/exceptions.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/exceptions.dart deleted file mode 100644 index 6f3c1e34b9..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/exceptions.dart +++ /dev/null @@ -1,5 +0,0 @@ -class PluginCompilationException implements Exception { - final String message; - - PluginCompilationException(this.message); -} 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 deleted file mode 100644 index 2f31ffbf1e..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart +++ /dev/null @@ -1,161 +0,0 @@ -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'; -import 'package:flowy_infra/theme.dart'; -import 'package:path/path.dart' as p; - -import 'plugin_type.dart'; - -typedef DynamicPluginLibrary = Iterable; - -/// A class that encapsulates dynamically loaded plugins for AppFlowy. -/// -/// This class can be modified to support loading node widget builders and other -/// plugins that are dynamically loaded at runtime for the editor. For now, -/// it only supports loading app themes. -class FlowyDynamicPlugin { - FlowyDynamicPlugin._({ - required String name, - required String path, - this.theme, - }) : _name = name, - _path = path; - - /// The plugins should be loaded into a folder with the extension `.flowy_plugin`. - static bool isPlugin(FileSystemEntity entity) => - entity is Directory && p.extension(entity.path).contains(ext); - - /// The extension for the plugin folder. - static const String ext = 'flowy_plugin'; - static String get lightExtension => ['light', 'json'].join('.'); - static String get darkExtension => ['dark', 'json'].join('.'); - - String get name => _name; - late final String _name; - - String get _fsPluginName => [name, ext].join('.'); - - final AppTheme? theme; - final String _path; - - Directory get source { - return Directory(_path); - } - - /// Loads and "compiles" loaded plugins. - /// - /// If the plugin loaded does not contain the `.flowy_plugin` extension, this - /// this method will throw an error. Likewise, if the plugin does not follow - /// the expected format, this method will throw an error. - static Future decode({required Directory src}) async { - // throw an error if the plugin does not follow the proper format. - if (!isPlugin(src)) { - throw PluginCompilationException( - 'The plugin directory must have the extension `.flowy_plugin`.', - ); - } - - // throws an error if the plugin does not follow the proper format. - final type = PluginType.from(src: src); - - switch (type) { - case PluginType.theme: - return _theme(src: src); - } - } - - /// Encodes the plugin in memory. The Directory given is not the actual - /// directory on the file system, but rather a virtual directory in memory. - /// - /// Instances of this class should always have a path on disk, otherwise a - /// compilation error will be thrown during the construction of this object. - Future encode() async { - final fs = MemoryFileSystem(); - final directory = fs.directory(_fsPluginName)..createSync(); - - final lightThemeFileName = '$name.$lightExtension'; - directory.childFile(lightThemeFileName).createSync(); - directory - .childFile(lightThemeFileName) - .writeAsStringSync(jsonEncode(theme!.lightTheme.toJson())); - - final darkThemeFileName = '$name.$darkExtension'; - directory.childFile(darkThemeFileName).createSync(); - directory - .childFile(darkThemeFileName) - .writeAsStringSync(jsonEncode(theme!.darkTheme.toJson())); - - return directory; - } - - /// Theme plugins should have the following format. - /// > directory.flowy_plugin // plugin root - /// > - theme.light.json // the light theme - /// > - theme.dark.json // the dark theme - /// - /// If the theme does not adhere to that format, it is considered an error. - static Future _theme({required Directory src}) async { - late final String name; - try { - name = p.basenameWithoutExtension(src.path).split('.').first; - } catch (e) { - throw PluginCompilationException( - 'The theme plugin does not adhere to the following format: `.flowy_plugin`.', - ); - } - - final light = src - .listSync() - .where((event) => - event is File && p.basename(event.path).contains(lightExtension)) - .first as File; - - final dark = src - .listSync() - .where((event) => - event is File && p.basename(event.path).contains(darkExtension)) - .first as File; - - late final FlowyColorScheme lightTheme; - late final FlowyColorScheme darkTheme; - - try { - lightTheme = FlowyColorScheme.fromJsonSoft( - await jsonDecode(await light.readAsString()), - ); - } catch (e) { - throw PluginCompilationException( - 'The light theme json file is not valid.', - ); - } - - try { - darkTheme = FlowyColorScheme.fromJsonSoft( - await jsonDecode(await dark.readAsString()), - Brightness.dark, - ); - } catch (e) { - throw PluginCompilationException( - 'The dark theme json file is not valid.', - ); - } - - final theme = AppTheme( - themeName: name, - builtIn: false, - lightTheme: lightTheme, - darkTheme: darkTheme, - ); - - return FlowyDynamicPlugin._( - name: theme.themeName, - path: src.path, - theme: theme, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart deleted file mode 100644 index 1b6f8a8f4a..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:io'; - -import 'package:flowy_infra/plugins/service/models/exceptions.dart'; -import 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart'; -import 'package:path/path.dart' as p; - -enum PluginType { - theme._(); - - const PluginType._(); - - factory PluginType.from({required Directory src}) { - if (_isTheme(src)) { - return PluginType.theme; - } - throw PluginCompilationException( - 'Could not determine the plugin type from source `$src`.'); - } - - static bool _isTheme(Directory plugin) { - final files = plugin.listSync(); - return files.any((entity) => - entity is File && - p - .basename(entity.path) - .endsWith(FlowyDynamicPlugin.lightExtension)) && - files.any((entity) => - entity is File && - p.basename(entity.path).endsWith(FlowyDynamicPlugin.darkExtension)); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart deleted file mode 100644 index bd81333be8..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flowy_infra/file_picker/file_picker_impl.dart'; - -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'location_service.dart'; -import 'models/flowy_dynamic_plugin.dart'; - -/// A service to maintain the state of the plugins for AppFlowy. -class FlowyPluginService { - FlowyPluginService._(); - static final FlowyPluginService _instance = FlowyPluginService._(); - static FlowyPluginService get instance => _instance; - - PluginLocationService _locationService = PluginLocationService( - fallback: getApplicationDocumentsDirectory(), - ); - - void setLocation(PluginLocationService locationService) => - _locationService = locationService; - - Future> get _targets async { - final location = await _locationService.location; - final targets = location.listSync().where(FlowyDynamicPlugin.isPlugin); - return targets.map((entity) => entity as Directory).toList(); - } - - /// Searches the [PluginLocationService.location] for plugins and compiles them. - Future get plugins async { - final List compiled = []; - for (final src in await _targets) { - final plugin = await FlowyDynamicPlugin.decode(src: src); - compiled.add(plugin); - } - return compiled; - } - - /// Chooses a plugin from the file system using FilePickerService and tries to compile it. - /// - /// If the operation is cancelled or the plugin is invalid, this method will return null. - static Future pick({FilePicker? service}) async { - service ??= FilePicker(); - - final result = await service.getDirectoryPath(); - - if (result == null) { - return null; - } - - final directory = Directory(result); - return FlowyDynamicPlugin.decode(src: directory); - } - - /// Searches the plugin registry for a plugin with the given name. - Future lookup({required String name}) async { - final library = await plugins; - return library - // cast to nullable type to allow return of null if not found. - .cast() - // null assert is fine here because the original list was non-nullable - .firstWhere((plugin) => plugin!.name == name, orElse: () => null); - } - - /// Adds a plugin to the registry. To construct a [FlowyDynamicPlugin] - /// use [FlowyDynamicPlugin.encode()] - Future addPlugin(FlowyDynamicPlugin plugin) async { - // try to compile the plugin before we add it to the registry. - final source = await plugin.encode(); - // add the plugin to the registry - final destionation = [ - (await _locationService.location).path, - p.basename(source.path), - ].join(Platform.pathSeparator); - - _copyDirectorySync(source, Directory(destionation)); - } - - /// Removes a plugin from the registry. - static Future removePlugin(FlowyDynamicPlugin plugin) async { - final target = plugin.source; - await target.delete(recursive: true); - } - - static void _copyDirectorySync(Directory source, Directory destination) { - if (!destination.existsSync()) { - destination.createSync(recursive: true); - } - - for (final child in source.listSync(recursive: false)) { - final newPath = p.join(destination.path, p.basename(child.path)); - if (child is File) { - File(newPath) - ..createSync(recursive: true) - ..writeAsStringSync(child.readAsStringSync()); - } else if (child is Directory) { - _copyDirectorySync(child, Directory(newPath)); - } - } - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart index f58dad95b5..3d0783e74e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart @@ -57,15 +57,16 @@ class Sizes { static double get hit => 40 * hitScale; static double get iconMed => 20; + + static double get sideBarMed => 225 * hitScale; + + static double get sideBarLg => 290 * hitScale; } class Corners { static const BorderRadius s3Border = BorderRadius.all(s3Radius); static const Radius s3Radius = Radius.circular(3); - static const BorderRadius s4Border = BorderRadius.all(s4Radius); - static const Radius s4Radius = Radius.circular(4); - static const BorderRadius s5Border = BorderRadius.all(s5Radius); static const Radius s5Radius = Radius.circular(5); @@ -80,7 +81,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.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart index a79ce01107..1f4f3584e1 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart @@ -1,68 +1,30 @@ import 'package:flowy_infra/colorscheme/colorscheme.dart'; -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; -import 'plugins/service/plugin_service.dart'; +import 'package:flutter/material.dart'; class BuiltInTheme { static const String defaultTheme = 'Default'; static const String dandelion = 'Dandelion'; - static const String lemonade = 'Lemonade'; static const String lavender = 'Lavender'; } class AppTheme { // metadata member - final bool builtIn; final String themeName; final FlowyColorScheme lightTheme; final FlowyColorScheme darkTheme; // static final Map _cachedJsonData = {}; const AppTheme({ - required this.builtIn, required this.themeName, required this.lightTheme, required this.darkTheme, }); - static const AppTheme fallback = AppTheme( - builtIn: true, - themeName: BuiltInTheme.defaultTheme, - lightTheme: DefaultColorScheme.light(), - darkTheme: DefaultColorScheme.dark(), - ); - - static Future> _plugins(FlowyPluginService service) async { - final plugins = await service.plugins; - return plugins.map((plugin) => plugin.theme).whereType(); - } - - static Iterable get builtins => themeMap.entries - .map( - (entry) => AppTheme( - builtIn: true, - themeName: entry.key, - lightTheme: entry.value[0], - darkTheme: entry.value[1], - ), - ) - .toList(); - - static Future> themes(FlowyPluginService service) async => - [ - ...builtins, - ...(await _plugins(service)), - ]; - - static Future fromName( - String themeName, { - FlowyPluginService? pluginService, - }) async { - pluginService ??= FlowyPluginService.instance; - for (final theme in await themes(pluginService)) { - if (theme.themeName == themeName) { - return theme; - } - } - throw ArgumentError('The theme $themeName does not exist.'); + factory AppTheme.fromName(String themeName) { + return AppTheme( + themeName: themeName, + lightTheme: FlowyColorScheme.builtIn(themeName, Brightness.light), + darkTheme: FlowyColorScheme.builtIn(themeName, Brightness.dark), + ); } } 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..ab5da1505d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -2,11 +2,30 @@ import 'package:flutter/material.dart'; @immutable class AFThemeExtension extends ThemeExtension { - static AFThemeExtension of(BuildContext context) => - Theme.of(context).extension()!; + final Color? warning; + final Color? success; - static AFThemeExtension? maybeOf(BuildContext context) => - Theme.of(context).extension(); + final Color tint1; + final Color tint2; + final Color tint3; + final Color tint4; + final Color tint5; + final Color tint6; + final Color tint7; + final Color tint8; + final Color tint9; + + final Color textColor; + final Color greyHover; + final Color greySelect; + final Color lightGreyHover; + final Color toggleOffFill; + final Color progressBarBGColor; + final Color toggleButtonBGColor; + + final TextStyle code; + final TextStyle callout; + final TextStyle caption; const AFThemeExtension({ required this.warning, @@ -25,70 +44,16 @@ class AFThemeExtension extends ThemeExtension { required this.lightGreyHover, required this.toggleOffFill, required this.textColor, - required this.secondaryTextColor, - required this.strongText, - required this.calloutBGColor, - required this.tableCellBGColor, - required this.calendarWeekendBGColor, required this.code, required this.callout, required this.caption, required this.progressBarBGColor, required this.toggleButtonBGColor, - 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; - final Color? success; - - final Color tint1; - final Color tint2; - final Color tint3; - final Color tint4; - final Color tint5; - final Color tint6; - final Color tint7; - final Color tint8; - final Color tint9; - - final Color textColor; - final Color secondaryTextColor; - final Color strongText; - final Color greyHover; - final Color greySelect; - final Color lightGreyHover; - final Color toggleOffFill; - final Color progressBarBGColor; - final Color toggleButtonBGColor; - final Color calloutBGColor; - final Color tableCellBGColor; - final Color calendarWeekendBGColor; - final Color gridRowCountColor; - - final TextStyle code; - final TextStyle callout; - final TextStyle caption; - - 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; + static AFThemeExtension of(BuildContext context) { + return Theme.of(context).extension()!; + } @override AFThemeExtension copyWith({ @@ -104,66 +69,40 @@ class AFThemeExtension extends ThemeExtension { Color? tint8, Color? tint9, Color? textColor, - Color? secondaryTextColor, - Color? strongText, - Color? calloutBGColor, - Color? tableCellBGColor, Color? greyHover, Color? greySelect, Color? lightGreyHover, Color? toggleOffFill, Color? progressBarBGColor, Color? toggleButtonBGColor, - Color? calendarWeekendBGColor, - Color? gridRowCountColor, TextStyle? code, TextStyle? callout, TextStyle? caption, - Color? background, - Color? onBackground, - Color? borderColor, - Color? scrollbarColor, - Color? scrollbarHoverColor, - Color? lightIconColor, - Color? toolbarHoverColor, - }) => - AFThemeExtension( - warning: warning ?? this.warning, - success: success ?? this.success, - tint1: tint1 ?? this.tint1, - tint2: tint2 ?? this.tint2, - tint3: tint3 ?? this.tint3, - tint4: tint4 ?? this.tint4, - tint5: tint5 ?? this.tint5, - tint6: tint6 ?? this.tint6, - tint7: tint7 ?? this.tint7, - tint8: tint8 ?? this.tint8, - tint9: tint9 ?? this.tint9, - textColor: textColor ?? this.textColor, - secondaryTextColor: secondaryTextColor ?? this.secondaryTextColor, - strongText: strongText ?? this.strongText, - calloutBGColor: calloutBGColor ?? this.calloutBGColor, - tableCellBGColor: tableCellBGColor ?? this.tableCellBGColor, - greyHover: greyHover ?? this.greyHover, - greySelect: greySelect ?? this.greySelect, - lightGreyHover: lightGreyHover ?? this.lightGreyHover, - toggleOffFill: toggleOffFill ?? this.toggleOffFill, - progressBarBGColor: progressBarBGColor ?? this.progressBarBGColor, - toggleButtonBGColor: toggleButtonBGColor ?? this.toggleButtonBGColor, - calendarWeekendBGColor: - calendarWeekendBGColor ?? this.calendarWeekendBGColor, - gridRowCountColor: gridRowCountColor ?? this.gridRowCountColor, - code: code ?? this.code, - callout: callout ?? this.callout, - 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, - ); + }) { + return AFThemeExtension( + warning: warning ?? this.warning, + success: success ?? this.success, + tint1: tint1 ?? this.tint1, + tint2: tint2 ?? this.tint2, + tint3: tint3 ?? this.tint3, + tint4: tint4 ?? this.tint4, + tint5: tint5 ?? this.tint5, + tint6: tint6 ?? this.tint6, + tint7: tint7 ?? this.tint7, + tint8: tint8 ?? this.tint8, + tint9: tint9 ?? this.tint9, + textColor: textColor ?? this.textColor, + greyHover: greyHover ?? this.greyHover, + greySelect: greySelect ?? this.greySelect, + lightGreyHover: lightGreyHover ?? this.lightGreyHover, + toggleOffFill: toggleOffFill ?? this.toggleOffFill, + progressBarBGColor: progressBarBGColor ?? this.progressBarBGColor, + toggleButtonBGColor: toggleButtonBGColor ?? this.toggleButtonBGColor, + code: code ?? this.code, + callout: callout ?? this.callout, + caption: caption ?? this.caption, + ); + } @override ThemeExtension lerp( @@ -184,19 +123,6 @@ class AFThemeExtension extends ThemeExtension { tint8: Color.lerp(tint8, other.tint8, t)!, tint9: Color.lerp(tint9, other.tint9, t)!, textColor: Color.lerp(textColor, other.textColor, t)!, - secondaryTextColor: Color.lerp( - secondaryTextColor, - other.secondaryTextColor, - t, - )!, - strongText: Color.lerp( - strongText, - other.strongText, - t, - )!, - calloutBGColor: Color.lerp(calloutBGColor, other.calloutBGColor, t)!, - tableCellBGColor: - Color.lerp(tableCellBGColor, other.tableCellBGColor, t)!, greyHover: Color.lerp(greyHover, other.greyHover, t)!, greySelect: Color.lerp(greySelect, other.greySelect, t)!, lightGreyHover: Color.lerp(lightGreyHover, other.lightGreyHover, t)!, @@ -205,22 +131,9 @@ class AFThemeExtension extends ThemeExtension { Color.lerp(progressBarBGColor, other.progressBarBGColor, t)!, toggleButtonBGColor: Color.lerp(toggleButtonBGColor, other.toggleButtonBGColor, t)!, - calendarWeekendBGColor: - Color.lerp(calendarWeekendBGColor, other.calendarWeekendBGColor, t)!, - gridRowCountColor: - Color.lerp(gridRowCountColor, other.gridRowCountColor, t)!, code: other.code, callout: other.callout, 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)!, ); } } @@ -245,38 +158,26 @@ enum FlowyTint { } } - static FlowyTint? fromId(String id) { - for (final value in FlowyTint.values) { - if (value.id == id) { - return value; - } + Color color(BuildContext context) { + switch (this) { + case FlowyTint.tint1: + return AFThemeExtension.of(context).tint1; + case FlowyTint.tint2: + return AFThemeExtension.of(context).tint2; + case FlowyTint.tint3: + return AFThemeExtension.of(context).tint3; + case FlowyTint.tint4: + return AFThemeExtension.of(context).tint4; + case FlowyTint.tint5: + return AFThemeExtension.of(context).tint5; + case FlowyTint.tint6: + return AFThemeExtension.of(context).tint6; + case FlowyTint.tint7: + return AFThemeExtension.of(context).tint7; + case FlowyTint.tint8: + return AFThemeExtension.of(context).tint8; + case FlowyTint.tint9: + return AFThemeExtension.of(context).tint9; } - 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, - }; - - String get id => switch (this) { - // DON'T change this name because it's saved in the database! - FlowyTint.tint1 => 'appflowy_them_color_tint1', - FlowyTint.tint2 => 'appflowy_them_color_tint2', - FlowyTint.tint3 => 'appflowy_them_color_tint3', - FlowyTint.tint4 => 'appflowy_them_color_tint4', - FlowyTint.tint5 => 'appflowy_them_color_tint5', - FlowyTint.tint6 => 'appflowy_them_color_tint6', - FlowyTint.tint7 => 'appflowy_them_color_tint7', - FlowyTint.tint8 => 'appflowy_them_color_tint8', - FlowyTint.tint9 => 'appflowy_them_color_tint9', - }; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/time/duration.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/time/duration.dart index a408470da9..5763ce1d46 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/time/duration.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/time/duration.dart @@ -1,8 +1,7 @@ import 'package:time/time.dart'; - export 'package:time/time.dart'; -class FlowyDurations { +class Durations { static Duration get fastest => .15.seconds; static Duration get fast => .25.seconds; 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 deleted file mode 100644 index 4f80f81e62..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:json_annotation/json_annotation.dart'; - -class ColorConverter implements JsonConverter { - const ColorConverter(); - - static const Color fallback = Colors.transparent; - - @override - Color fromJson(String radixString) { - final int? color = int.tryParse(radixString); - return color == null ? fallback : Color(color); - } - - @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(); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/uuid.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/uuid.dart index 1f4c903625..df8aee9dda 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/uuid.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/uuid.dart @@ -1,19 +1,5 @@ -import 'package:uuid/data.dart'; -import 'package:uuid/rng.dart'; import 'package:uuid/uuid.dart'; -const _uuid = Uuid(); - String uuid() { - return _uuid.v4(); -} - -String fixedUuid(int seed, UuidType type) { - return _uuid.v4(config: V4Options(null, MathRNG(seed: seed + type.index))); -} - -enum UuidType { - // 0.6.0 - publicSpace, - privateSpace, + return const Uuid().v4(); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index 9bf0245dc0..8991da7ce4 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,48 @@ environment: dependencies: flutter: sdk: flutter - json_annotation: ^4.7.0 - path_provider: ^2.0.15 - path: ^1.8.2 - time: ">=2.0.0" + time: '>=2.0.0' uuid: ">=2.2.2" - bloc: ^9.0.0 - freezed_annotation: ^2.1.0 - file_picker: ^8.0.2 - file: ^7.0.0 - analyzer: 6.11.0 + flutter_svg: ^2.0.6 dev_dependencies: - build_runner: ^2.4.9 - flutter_lints: ^3.0.1 - freezed: ^2.4.7 - json_serializable: ^6.5.4 + flutter_test: + sdk: flutter + flutter_lints: ^2.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. +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/android/.project b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/.project index 4141b13cf1..77aded223a 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/android/.project +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/.project @@ -22,12 +22,12 @@ - 1693395487121 + 1626576261667 30 org.eclipse.core.resources.regexFilterMatcher - node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/android/.settings/org.eclipse.buildship.core.prefs b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/.settings/org.eclipse.buildship.core.prefs index d76c0b7ac1..e8895216fd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/android/.settings/org.eclipse.buildship.core.prefs +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/.settings/org.eclipse.buildship.core.prefs @@ -1,13 +1,2 @@ -arguments=--init-script /var/folders/th/tfqrqcp12kvgzs3c3z0xqxlc0000gn/T/d146c9752a26f79b52047fb6dc6ed385d064e120494f96f08ca63a317c41f94c.gradle --init-script /var/folders/th/tfqrqcp12kvgzs3c3z0xqxlc0000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle -auto.sync=false -build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.4.2)) connection.project.dir= eclipse.preferences.version=1 -gradle.user.home= -java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home -jvm.arguments= -offline.mode=false -override.workspace.settings=true -show.console.view=true -show.executions.view=true diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/android/build.gradle b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/build.gradle index 68cb27f4b0..d129628362 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/android/build.gradle +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/build.gradle @@ -2,14 +2,14 @@ group 'com.example.flowy_infra_ui' version '1.0' buildscript { - ext.kotlin_version = '1.8.0' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:4.1.0' } } @@ -23,7 +23,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 33 + compileSdkVersion 30 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/kotlin/com/example/flowy_infra_ui/FlowyInfraUiPlugin.kt b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/kotlin/com/example/flowy_infra_ui/FlowyInfraUiPlugin.kt index 1775aeaf9c..5d98dd8f10 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/kotlin/com/example/flowy_infra_ui/FlowyInfraUiPlugin.kt +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/kotlin/com/example/flowy_infra_ui/FlowyInfraUiPlugin.kt @@ -8,8 +8,8 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -/** FlowyInfraUIPlugin */ -class FlowyInfraUIPlugin: FlutterPlugin, MethodCallHandler { +/** FlowyInfraUiPlugin */ +class FlowyInfraUiPlugin: FlutterPlugin, MethodCallHandler { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/.settings/org.eclipse.buildship.core.prefs b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/.settings/org.eclipse.buildship.core.prefs index 25e4212285..b1886adb46 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/.settings/org.eclipse.buildship.core.prefs +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/.settings/org.eclipse.buildship.core.prefs @@ -1,13 +1,2 @@ -arguments= -auto.sync=false -build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) -connection.project.dir= +connection.project.dir=.. eclipse.preferences.version=1 -gradle.user.home= -java.home=/Library/Java/JavaVirtualMachines/jdk11.0.5-zulu.jdk/Contents/Home -jvm.arguments= -offline.mode=false -override.workspace.settings=true -show.console.view=true -show.executions.view=true diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart index 875440c04e..954930e9cd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart @@ -5,7 +5,7 @@ import '../keyboard/keyboard_screen.dart'; import 'demo_item.dart'; class HomeScreen extends StatelessWidget { - const HomeScreen({super.key}); + const HomeScreen({Key? key}) : super(key: key); static List items = [ SectionHeaderItem('Widget Demos'), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart index 2b1a1cf59e..fde544365b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart @@ -19,7 +19,7 @@ class KeyboardItem extends DemoItem { } class KeyboardScreen extends StatefulWidget { - const KeyboardScreen({super.key}); + const KeyboardScreen({Key? key}) : super(key: key); @override State createState() => _KeyboardScreenState(); @@ -30,12 +30,6 @@ class _KeyboardScreenState extends State { final TextEditingController _controller = TextEditingController(text: 'Hello Flowy'); - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart index ecaa6266c6..b0ae8d3c32 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart @@ -9,14 +9,14 @@ void main() { } class ExampleApp extends StatelessWidget { - const ExampleApp({super.key}); + const ExampleApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return const MaterialApp( - builder: overlayManagerBuilder, + return MaterialApp( + builder: overlayManagerBuilder(), title: "Flowy Infra Title", - home: HomeScreen(), + home: const HomeScreen(), ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart index bec711cc98..ae5cf7ef67 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart @@ -44,7 +44,7 @@ class OverlayDemoConfiguration extends ChangeNotifier { } class OverlayScreen extends StatelessWidget { - const OverlayScreen({super.key}); + const OverlayScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Podfile b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Podfile index fe733905db..dade8dfad0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Podfile +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.13' +platform :osx, '10.11' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml index 8c7793d7cc..b9c8ec058e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml @@ -4,8 +4,7 @@ description: Demonstrates how to use the flowy_infra_ui plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - flutter: ">=3.22.0" - sdk: ">=3.1.5 <4.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: @@ -21,7 +20,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.1 + flutter_lints: ^2.0.1 flutter: uses-material-design: true 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..2f375be367 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" @@ -16,4 +16,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.1 + flutter_lints: ^2.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..224d7bf47f 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: @@ -18,11 +18,11 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.1 + flutter_lints: ^2.0.1 flutter: plugin: 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/ios/Classes/FlowyInfraUIPlugin.h b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.h deleted file mode 100644 index 85c878d9fd..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface FlowyInfraUIPlugin : NSObject -@end diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.m b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.m deleted file mode 100644 index 58f19ce516..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "FlowyInfraUIPlugin.h" -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "flowy_infra_ui-Swift.h" -#endif - -@implementation FlowyInfraUIPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftFlowyInfraUIPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUiPlugin.h b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUiPlugin.h new file mode 100644 index 0000000000..9f353812ba --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUiPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FlowyInfraUiPlugin : NSObject +@end diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUiPlugin.m b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUiPlugin.m new file mode 100644 index 0000000000..6609bdcf24 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUiPlugin.m @@ -0,0 +1,15 @@ +#import "FlowyInfraUiPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "flowy_infra_ui-Swift.h" +#endif + +@implementation FlowyInfraUiPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFlowyInfraUiPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUIPlugin.swift b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUIPlugin.swift deleted file mode 100644 index 3e295388e7..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUIPlugin.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Flutter -import UIKit - -public class SwiftFlowyInfraUIPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "flowy_infra_ui", binaryMessenger: registrar.messenger()) - let instance = SwiftFlowyInfraUIPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - result("iOS " + UIDevice.current.systemVersion) - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUiPlugin.swift b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUiPlugin.swift new file mode 100644 index 0000000000..5459739470 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUiPlugin.swift @@ -0,0 +1,14 @@ +import Flutter +import UIKit + +public class SwiftFlowyInfraUiPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "flowy_infra_ui", binaryMessenger: registrar.messenger()) + let instance = SwiftFlowyInfraUiPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart index 7bbcbf0949..59a2bc6533 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart @@ -1,3 +1,8 @@ +import 'package:flutter/material.dart'; + // MARK: - Shared Builder + +typedef WidgetBuilder = Widget Function(); + typedef IndexedCallback = void Function(int index); typedef IndexedValueCallback = void Function(T value, int index); 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..7c830775c8 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,23 +1,21 @@ // Basis -export '/widget/flowy_tooltip.dart'; -export '/widget/separated_flex.dart'; -export '/widget/spacing.dart'; export 'basis.dart'; -export 'src/flowy_overlay/appflowy_popover.dart'; -export 'src/flowy_overlay/flowy_dialog.dart'; + +// Keyboard +export 'src/keyboard/keyboard_visibility_detector.dart'; + // Overlay export 'src/flowy_overlay/flowy_overlay.dart'; export 'src/flowy_overlay/list_overlay.dart'; export 'src/flowy_overlay/option_overlay.dart'; -// Keyboard -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 'src/flowy_overlay/flowy_dialog.dart'; +export 'src/flowy_overlay/appflowy_popover.dart'; export 'style_widget/text.dart'; export 'style_widget/text_field.dart'; + +export 'style_widget/button.dart'; +export 'style_widget/icon_button.dart'; +export 'style_widget/scrolling/styled_scroll_bar.dart'; +export '/widget/spacing.dart'; +export 'style_widget/scrolling/styled_list.dart'; +export 'style_widget/color_picker.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..6062cae35f 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,33 +1,28 @@ 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 void Function()? onClose; + final Future Function()? canClose; + final PopoverMutex? mutex; + final Offset? offset; + final bool asBarrier; + final EdgeInsets margin; + final EdgeInsets windowPadding; + final Decoration? decoration; + const AppFlowyPopover({ - super.key, + Key? key, required this.child, required this.popupBuilder, this.direction = PopoverDirection.rightWithTopAligned, - this.onOpen, this.onClose, this.canClose, this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600), @@ -38,76 +33,13 @@ class AppFlowyPopover extends StatelessWidget { this.asBarrier = false, this.margin = const EdgeInsets.all(6), this.windowPadding = const EdgeInsets.all(8.0), - 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; + this.decoration, + }) : super(key: key); @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, direction: direction, @@ -116,83 +48,50 @@ class AppFlowyPopover extends StatelessWidget { triggerActions: triggerActions, windowPadding: windowPadding, 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) { + final child = popupBuilder(context); + return _PopoverContainer( + constraints: constraints, + margin: margin, + decoration: decoration, + child: child, + ); + }, child: child, ); } } class _PopoverContainer extends StatelessWidget { - const _PopoverContainer({ - this.decorationColor, - this.borderRadius, - this.decoration, - required this.child, - required this.margin, - required this.constraints, - }); - final Widget child; final BoxConstraints constraints; final EdgeInsets margin; - final Color? decorationColor; - final BorderRadius? borderRadius; final Decoration? decoration; + const _PopoverContainer({ + required this.child, + required this.margin, + required this.constraints, + required this.decoration, + Key? key, + }) : super(key: key); + @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..cd37051220 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,10 +1,9 @@ import 'package:flutter/material.dart'; +import 'dart:math'; const _overlayContainerPadding = EdgeInsets.symmetric(vertical: 12); const overlayContainerMaxWidth = 760.0; const overlayContainerMinWidth = 320.0; -const _defaultInsetPadding = - EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0); class FlowyDialog extends StatelessWidget { const FlowyDialog({ @@ -15,10 +14,6 @@ class FlowyDialog extends StatelessWidget { this.constraints, this.padding = _overlayContainerPadding, this.backgroundColor, - this.expandHeight = true, - this.alignment, - this.insetPadding, - this.width, }); final Widget? title; @@ -27,41 +22,28 @@ class FlowyDialog extends StatelessWidget { final BoxConstraints? constraints; final EdgeInsets padding; final Color? backgroundColor; - final bool expandHeight; - - // Position of the Dialog - final Alignment? alignment; - - // Inset of the Dialog - final EdgeInsets? insetPadding; - - final double? width; @override Widget build(BuildContext context) { final windowSize = MediaQuery.of(context).size; final size = windowSize * 0.7; - return SimpleDialog( - alignment: alignment, - insetPadding: insetPadding ?? _defaultInsetPadding, - contentPadding: EdgeInsets.zero, - backgroundColor: backgroundColor ?? Theme.of(context).cardColor, - title: title, - shape: shape ?? - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - clipBehavior: Clip.antiAliasWithSaveLayer, - children: [ - Material( - type: MaterialType.transparency, - child: Container( - height: expandHeight ? size.height : null, - width: width ?? size.width, - constraints: constraints, - child: child, - ), - ) - ], - ); + contentPadding: EdgeInsets.zero, + backgroundColor: backgroundColor ?? Theme.of(context).cardColor, + title: title, + shape: shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + children: [ + Material( + type: MaterialType.transparency, + child: Container( + height: size.height, + 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..f49c3e3bc3 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 @@ -1,12 +1,10 @@ // ignore_for_file: unused_element 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 @@ -68,9 +66,11 @@ class FlowyOverlayStyle { final GlobalKey _key = GlobalKey(); /// Invoke this method in app generation process -Widget overlayManagerBuilder(BuildContext context, Widget? child) { - assert(child != null, 'Child can\'t be null.'); - return FlowyOverlay(key: _key, child: child!); +TransitionBuilder overlayManagerBuilder() { + return (context, child) { + assert(child != null, 'Child can\'t be null.'); + return FlowyOverlay(key: _key, child: child!); + }; } abstract mixin class FlowyOverlayDelegate { @@ -79,7 +79,7 @@ abstract mixin class FlowyOverlayDelegate { } class FlowyOverlay extends StatefulWidget { - const FlowyOverlay({super.key, required this.child}); + const FlowyOverlay({Key? key, required this.child}) : super(key: key); final Widget child; @@ -293,7 +293,7 @@ class FlowyOverlayState extends State { RenderObject renderObject = anchorContext.findRenderObject()!; assert( renderObject is RenderBox, - 'Unexpecteded non-RenderBox render object caught.', + 'Unexpected non-RenderBox render object caught.', ); final renderBox = renderObject as RenderBox; targetAnchorPosition = renderBox.localToGlobal(Offset.zero); @@ -315,11 +315,11 @@ class FlowyOverlayState extends State { ), child: Focus( focusNode: focusNode, - onKeyEvent: (node, event) { + onKey: (node, event) { KeyEventResult result = KeyEventResult.ignored; for (final ShortcutActivator activator in _keyboardShortcutBindings.keys) { - if (activator.accepts(event, HardwareKeyboard.instance)) { + if (activator.accepts(event, RawKeyboard.instance)) { _keyboardShortcutBindings[activator]!.call(identifier); result = KeyEventResult.handled; } @@ -342,17 +342,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 +391,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..eecc81283c 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 @@ -17,13 +17,13 @@ class ListOverlayFooter { class ListOverlay extends StatelessWidget { const ListOverlay({ - super.key, + Key? key, required this.itemBuilder, this.itemCount = 0, this.controller, this.constraints = const BoxConstraints(), this.footer, - }); + }) : super(key: key); final IndexedWidgetBuilder itemBuilder; final int itemCount; @@ -117,8 +117,8 @@ class OverlayContainer extends StatelessWidget { required this.child, this.constraints, this.padding = overlayContainerPadding, - super.key, - }); + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -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/src/flowy_overlay/option_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart index 5248665c41..d6dfc4607d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart @@ -10,11 +10,11 @@ class OptionItem { class OptionOverlay extends StatelessWidget { const OptionOverlay({ - super.key, + Key? key, required this.items, this.onHover, this.onTap, - }); + }) : super(key: key); final List items; final IndexedValueCallback? onHover; @@ -69,8 +69,8 @@ class OptionOverlay extends StatelessWidget { class _OptionListItem extends StatelessWidget { const _OptionListItem( this.value, { - super.key, - }); + Key? key, + }) : super(key: key); final T value; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart index 148a812910..45faebc07d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; class AutoUnfocus extends StatelessWidget { const AutoUnfocus({ - super.key, + Key? key, required this.child, - }); + }) : super(key: key); final Widget child; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart index cbe9a07908..643ddd94b1 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart'; class KeyboardVisibilityDetector extends StatefulWidget { const KeyboardVisibilityDetector({ - super.key, + Key? key, required this.child, this.onKeyboardVisibilityChange, - }); + }) : super(key: key); final Widget child; final void Function(bool)? onKeyboardVisibilityChange; @@ -57,9 +57,10 @@ class _KeyboardVisibilityDetectorState class _KeyboardVisibilityDetectorInheritedWidget extends InheritedWidget { const _KeyboardVisibilityDetectorInheritedWidget({ + Key? key, required this.isKeyboardVisible, - required super.child, - }); + required Widget child, + }) : super(key: key, child: child); final bool isKeyboardVisible; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart index cc54a8ba00..47cd1cd8e9 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart @@ -4,9 +4,9 @@ class FlowyBarTitle extends StatelessWidget { final String title; const FlowyBarTitle({ - super.key, + Key? key, required this.title, - }); + }) : super(key: key); @override Widget build(BuildContext context) { 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..c1444b531e 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 @@ -1,152 +1,16 @@ -import 'dart:io'; - 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_infra_ui/style_widget/text.dart'; 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; @@ -157,17 +21,9 @@ class FlowyButton extends StatelessWidget { 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 Color? backgroundColor; - final bool resetHoverOnRebuild; const FlowyButton({ - super.key, + Key? key, required this.text, this.onTap, this.onSecondaryTap, @@ -183,55 +39,38 @@ class FlowyButton extends StatelessWidget { 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.backgroundColor, - this.resetHoverOnRebuild = true, - }); + }) : super(key: key); @override Widget build(BuildContext context) { - final color = hoverColor ?? Theme.of(context).colorScheme.secondary; - final alpha = (255 * disableOpacity).toInt(); - color.withAlpha(alpha); - - 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, - child: _render(context), + if (!disable) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + onSecondaryTap: onSecondaryTap, + child: FlowyHover( + style: HoverStyle( + borderRadius: radius ?? Corners.s6Border, + hoverColor: hoverColor ?? Theme.of(context).colorScheme.secondary, + ), + onHover: onHover, + isSelected: () => isSelected, + builder: (context, onHover) => _render(), + ), + ); + } else { + return Opacity( + opacity: disableOpacity, + child: MouseRegion( + cursor: SystemMouseCursors.forbidden, + child: _render(), + ), ); } - - 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!), - backgroundColor: backgroundColor ?? Colors.transparent, - ), - onHover: disable ? null : onHover, - isSelected: () => isSelected, - builder: (context, onHover) => _render(context), - ), - ); } - Widget _render(BuildContext context) { - final List children = []; + Widget _render() { + List children = List.empty(growable: true); if (leftIcon != null) { children.add( @@ -240,25 +79,20 @@ class FlowyButton extends StatelessWidget { child: leftIcon!, ), ); - children.add(HSpace(iconPadding)); + children.add(const HSpace(6)); } - if (expandText) { - children.add(Expanded(child: text)); - } else { - children.add(text); - } + children.add(Expanded(child: text)); if (rightIcon != null) { - children.add(HSpace(iconPadding)); + children.add(const HSpace(6)); // No need to define the size of rightIcon. Just use its intrinsic width children.add(rightIcon!); } Widget child = Row( - mainAxisAlignment: mainAxisAlignment, + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min, children: children, ); @@ -266,41 +100,11 @@ class FlowyButton extends StatelessWidget { child = IntrinsicWidth(child: child); } - var decoration = this.decoration; - - if (decoration == null && - (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, - ); - } - } - return Container( decoration: decoration, child: Padding( - padding: margin ?? - const EdgeInsets.symmetric( - horizontal: 6, - vertical: 4, - ), + padding: + margin ?? const EdgeInsets.symmetric(horizontal: 10, vertical: 2), child: child, ), ); @@ -308,65 +112,9 @@ class FlowyButton extends StatelessWidget { } class FlowyTextButton extends StatelessWidget { - const FlowyTextButton( - this.text, { - super.key, - this.onPressed, - this.fontSize, - this.fontColor, - this.fontHoverColor, - this.overflow = TextOverflow.ellipsis, - this.fontWeight, - this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - this.hoverColor, - this.fillColor, - this.heading, - this.radius, - this.mainAxisAlignment = MainAxisAlignment.start, - this.tooltip, - this.constraints = const BoxConstraints(minWidth: 0.0, minHeight: 0.0), - 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; - final Color? fontHoverColor; final double? fontSize; final TextOverflow overflow; @@ -383,109 +131,16 @@ class FlowyTextButton extends StatelessWidget { final TextDecoration? decoration; final String? fontFamily; - final bool isDangerous; - final Color? borderColor; - final double? lineHeight; - @override - Widget build(BuildContext context) { - List children = []; - if (heading != null) { - children.add(heading!); - children.add(const HSpace(8)); - } - children.add(Text( - text, - overflow: overflow, - textAlign: TextAlign.center, - )); - - Widget child = Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: mainAxisAlignment, - children: children, - ); - - child = ConstrainedBox( - constraints: constraints, - child: TextButton( - onPressed: onPressed, - focusNode: FocusNode(skipTraversal: onPressed == null), - style: ButtonStyle( - overlayColor: const WidgetStatePropertyAll(Colors.transparent), - splashFactory: NoSplash.splashFactory, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: WidgetStateProperty.all(padding), - elevation: WidgetStateProperty.all(0), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - side: BorderSide( - color: borderColor ?? - (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, - ), - ), - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.hovered)) { - return hoverColor ?? - (isDangerous - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.secondary); - } - - return fillColor ?? - (isDangerous - ? Colors.transparent - : Theme.of(context).colorScheme.secondaryContainer); - }, - ), - foregroundColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.hovered)) { - return fontHoverColor ?? - (fontColor ?? Theme.of(context).colorScheme.onSurface); - } - - return fontColor ?? Theme.of(context).colorScheme.onSurface; - }, - ), - ), - child: child, - ), - ); - - if (tooltip != null) { - child = FlowyTooltip(message: tooltip!, child: child); - } - - if (onPressed == null) { - child = ExcludeFocus(child: child); - } - - return child; - } -} - -class FlowyRichTextButton extends StatelessWidget { - const FlowyRichTextButton( + // final HoverDisplayConfig? hoverDisplay; + const FlowyTextButton( this.text, { - super.key, + Key? key, this.onPressed, + this.fontSize, + this.fontColor, this.overflow = TextOverflow.ellipsis, + this.fontWeight, this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), this.hoverColor, this.fillColor, @@ -495,8 +150,72 @@ class FlowyRichTextButton extends StatelessWidget { this.tooltip, this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0), this.decoration, - }); + this.fontFamily, + }) : super(key: key); + @override + Widget build(BuildContext context) { + List children = []; + if (heading != null) { + children.add(heading!); + children.add(const HSpace(6)); + } + children.add( + FlowyText( + text, + overflow: overflow, + fontWeight: fontWeight, + fontSize: fontSize, + color: fontColor, + textAlign: TextAlign.center, + decoration: decoration, + fontFamily: fontFamily, + ), + ); + + Widget child = Padding( + padding: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: mainAxisAlignment, + children: children, + ), + ); + + child = RawMaterialButton( + visualDensity: VisualDensity.compact, + hoverElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), + fillColor: fillColor ?? Theme.of(context).colorScheme.secondaryContainer, + hoverColor: + hoverColor ?? Theme.of(context).colorScheme.secondaryContainer, + focusColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + elevation: 0, + constraints: constraints, + onPressed: () {}, + child: child, + ); + + child = IgnoreParentGestureWidget( + onPress: onPressed, + child: child, + ); + + if (tooltip != null) { + child = Tooltip( + message: tooltip!, + child: child, + ); + } + + return child; + } +} + +class FlowyRichTextButton extends StatelessWidget { final InlineSpan text; final TextOverflow overflow; @@ -512,6 +231,23 @@ class FlowyRichTextButton extends StatelessWidget { final TextDecoration? decoration; + // final HoverDisplayConfig? hoverDisplay; + const FlowyRichTextButton( + this.text, { + Key? key, + this.onPressed, + this.overflow = TextOverflow.ellipsis, + this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + this.hoverColor, + this.fillColor, + this.heading, + this.radius, + this.mainAxisAlignment = MainAxisAlignment.start, + this.tooltip, + this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0), + this.decoration, + }) : super(key: key); + @override Widget build(BuildContext context) { List children = []; @@ -520,7 +256,11 @@ class FlowyRichTextButton extends StatelessWidget { children.add(const HSpace(6)); } children.add( - RichText(text: text, overflow: overflow, textAlign: TextAlign.center), + RichText( + text: text, + overflow: overflow, + textAlign: TextAlign.center, + ), ); Widget child = Padding( @@ -533,6 +273,7 @@ class FlowyRichTextButton extends StatelessWidget { ); child = RawMaterialButton( + visualDensity: VisualDensity.compact, hoverElevation: 0, highlightElevation: 0, shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), @@ -547,10 +288,16 @@ class FlowyRichTextButton extends StatelessWidget { child: child, ); - child = IgnoreParentGestureWidget(onPress: onPressed, child: child); + child = IgnoreParentGestureWidget( + onPress: onPressed, + child: child, + ); if (tooltip != null) { - child = FlowyTooltip(message: tooltip!, child: child); + child = Tooltip( + message: tooltip!, + child: child, + ); } return child; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart index 94b1d3d40e..371ee123ed 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart @@ -4,9 +4,9 @@ class FlowyCloseButton extends StatelessWidget { final VoidCallback? onPressed; const FlowyCloseButton({ - super.key, + Key? key, this.onPressed, - }); + }) : super(key: key); @override Widget build(BuildContext context) { 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..bc372e24d8 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 @@ -1,30 +1,28 @@ +import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; class FlowyColorOption { const FlowyColorOption({ required this.color, - required this.i18n, - required this.id, + required this.name, }); final Color color; - final String i18n; - final String id; + final String name; } class FlowyColorPicker extends StatelessWidget { final List colors; final Color? selected; - final Function(FlowyColorOption option, int index)? onTap; + final Function(Color color, int index)? onTap; final double separatorSize; final double iconSize; final double itemHeight; final Border? border; const FlowyColorPicker({ - super.key, + Key? key, required this.colors, this.selected, this.onTap, @@ -32,12 +30,13 @@ class FlowyColorPicker extends StatelessWidget { this.iconSize = 16, this.itemHeight = 32, this.border, - }); + }) : super(key: key); @override Widget build(BuildContext context) { return ListView.separated( shrinkWrap: true, + controller: ScrollController(), separatorBuilder: (context, index) { return VSpace(separatorSize); }, @@ -55,52 +54,30 @@ class FlowyColorPicker extends StatelessWidget { ) { Widget? checkmark; if (selected == option.color) { - checkmark = const FlowySvg(FlowySvgData("grid/checkmark")); + checkmark = svgWidget("grid/checkmark"); } - final colorIcon = ColorOptionIcon( - color: option.color, - iconSize: iconSize, + final colorIcon = SizedBox.square( + dimension: iconSize, + child: Container( + decoration: BoxDecoration( + color: option.color, + shape: BoxShape.circle, + // border: border, + ), + ), ); return SizedBox( height: itemHeight, child: FlowyButton( - text: FlowyText(option.i18n), + text: FlowyText.medium(option.name), leftIcon: colorIcon, rightIcon: checkmark, - iconPadding: 10, onTap: () { - onTap?.call(option, i); + onTap?.call(option.color, i); }, ), ); } } - -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/container.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart index 32f9809f68..fc91998c1f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart @@ -14,7 +14,7 @@ class FlowyContainer extends StatelessWidget { final BoxBorder? border; const FlowyContainer(this.color, - {super.key, + {Key? key, this.borderRadius, this.shadows, this.child, @@ -23,7 +23,8 @@ class FlowyContainer extends StatelessWidget { this.align, this.margin, this.duration, - this.border}); + this.border}) + : super(key: key); @override Widget build(BuildContext context) { @@ -32,7 +33,7 @@ class FlowyContainer extends StatelessWidget { height: height, margin: margin, alignment: align, - duration: duration ?? FlowyDurations.medium, + duration: duration ?? Durations.medium, decoration: BoxDecoration( color: color, borderRadius: borderRadius, 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..1a4b96ecb5 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 @@ -7,12 +7,10 @@ class FlowyDecoration { double spreadRadius = 0, double blurRadius = 20, Offset offset = Offset.zero, - double borderRadius = 6, - BoxBorder? border, }) { return BoxDecoration( color: boxColor, - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + borderRadius: const BorderRadius.all(Radius.circular(6)), boxShadow: [ BoxShadow( color: boxShadow, @@ -21,7 +19,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/extension.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart index cf23694078..04ee12d4f3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart @@ -1,27 +1,26 @@ import 'package:flutter/material.dart'; export 'package:styled_widget/styled_widget.dart'; -class TopBorder extends StatelessWidget { - const TopBorder({ - super.key, - this.width = 1.0, - this.color = Colors.grey, - required this.child, - }); +extension FlowyStyledWidget on Widget { + Widget bottomBorder({double width = 1.0, Color color = Colors.grey}) { + return DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(width: width, color: color), + ), + ), + child: this, + ); + } - final Widget child; - final double width; - final Color color; - - @override - Widget build(BuildContext context) { + Widget topBorder({double width = 1.0, Color color = Colors.grey}) { return DecoratedBox( decoration: BoxDecoration( border: Border( top: BorderSide(width: width, color: color), ), ), - child: child, + child: this, ); } } 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..f2c6843956 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 @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +// ignore: unused_import +import 'package:flowy_infra/time/duration.dart'; typedef HoverBuilder = Widget Function(BuildContext context, bool onHover); @@ -11,10 +13,6 @@ class FlowyHover extends StatefulWidget { final void Function(bool)? onHover; final MouseCursor? cursor; - /// Reset the hover state when the parent widget get rebuild. - /// Default to true. - final bool resetHoverOnRebuild; - /// Determined whether the [builder] should get called when onEnter/onExit /// happened /// @@ -24,16 +22,15 @@ class FlowyHover extends StatefulWidget { final bool Function()? buildWhenOnHover; const FlowyHover({ - super.key, + Key? key, this.builder, this.child, this.style, this.isSelected, this.onHover, this.cursor, - this.resetHoverOnRebuild = true, this.buildWhenOnHover, - }); + }) : super(key: key); @override State createState() => _FlowyHoverState(); @@ -44,11 +41,8 @@ class _FlowyHoverState extends State { @override void didUpdateWidget(covariant FlowyHover oldWidget) { - if (widget.resetHoverOnRebuild) { - // Reset the _onHover to false when the parent widget get rebuild. - _onHover = false; - } - + // Reset the _onHover to false when the parent widget get rebuild. + _onHover = false; super.didUpdateWidget(oldWidget); } @@ -57,32 +51,53 @@ 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; + + if (widget.buildWhenOnHover?.call() ?? true) { + setState(() => _onHover = true); + if (widget.onHover != null) { + widget.onHover!(true); + } + } + }, + onExit: (p) { + if (_onHover == false) return; + + if (widget.buildWhenOnHover?.call() ?? true) { + setState(() => _onHover = false); + if (widget.onHover != null) { + widget.onHover!(false); + } + } + }, + child: renderWidget(), ); } - void _setOnHover(bool isHovering) { - if (isHovering == _onHover) return; + Widget renderWidget() { + var showHover = _onHover; + if (!showHover && widget.isSelected != null) { + showHover = widget.isSelected!(); + } - if (widget.buildWhenOnHover?.call() ?? true) { - setState(() => _onHover = isHovering); - if (widget.onHover != null) { - widget.onHover!(isHovering); - } + 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(color: style.backgroundColor, child: child); } } } class HoverStyle { - final BoxBorder? border; + final Color borderColor; + final double borderWidth; final Color? hoverColor; final Color? foregroundColorOnHover; final BorderRadius borderRadius; @@ -90,37 +105,33 @@ 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, this.hoverColor, this.foregroundColorOnHover, }); - - const HoverStyle.transparent({ - this.borderRadius = const BorderRadius.all(Radius.circular(6)), - this.contentMargin = EdgeInsets.zero, - this.backgroundColor = Colors.transparent, - this.foregroundColorOnHover, - }) : hoverColor = Colors.transparent, - border = null; } class FlowyHoverContainer extends StatelessWidget { final HoverStyle style; final Widget child; - final bool applyStyle; const FlowyHoverContainer({ - super.key, + Key? key, required this.child, required this.style, - this.applyStyle = false, - }); + }) : super(key: key); @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 +150,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..c8d089799e 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,9 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; +import 'dart:math'; +import 'package:flowy_infra/image.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; @@ -20,11 +18,9 @@ class FlowyIconButton extends StatelessWidget { final String? tooltipText; final InlineSpan? richTooltipText; final bool preferBelow; - final BoxDecoration? decoration; - final bool? isSelected; const FlowyIconButton({ - super.key, + Key? key, this.width = 30, this.height, this.onPressed, @@ -33,15 +29,14 @@ class FlowyIconButton extends StatelessWidget { this.iconColorOnHover, this.iconPadding = EdgeInsets.zero, this.radius, - this.decoration, this.tooltipText, this.richTooltipText, this.preferBelow = true, - this.isSelected, required this.icon, - }) : assert((richTooltipText != null && tooltipText == null) || + }) : assert((richTooltipText != null && tooltipText == null) || (richTooltipText == null && tooltipText != null) || - (richTooltipText == null && tooltipText == null)); + (richTooltipText == null && tooltipText == null)), + super(key: key); @override Widget build(BuildContext context) { @@ -54,50 +49,45 @@ class FlowyIconButton extends StatelessWidget { assert(size.width > iconPadding.horizontal); assert(size.height > iconPadding.vertical); - child = Padding( - padding: iconPadding, - child: Center(child: child), - ); + final childWidth = min(size.width - iconPadding.horizontal, + size.height - iconPadding.vertical); + final childSize = Size(childWidth, childWidth); - if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { - 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. - ), - resetHoverOnRebuild: false, - child: child, - ); - } - - return Container( + return ConstrainedBox( constraints: BoxConstraints.tightFor( width: size.width, height: size.height, ), - decoration: decoration, - child: FlowyTooltip( + child: Tooltip( preferBelow: preferBelow, message: tooltipMessage, richMessage: richTooltipText, + showDuration: Duration.zero, child: RawMaterialButton( - clipBehavior: Clip.antiAlias, + visualDensity: VisualDensity.compact, hoverElevation: 0, highlightElevation: 0, shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), fillColor: fillColor, - hoverColor: Colors.transparent, + hoverColor: hoverColor ?? Theme.of(context).colorScheme.secondary, focusColor: Colors.transparent, splashColor: Colors.transparent, highlightColor: Colors.transparent, elevation: 0, onPressed: onPressed, - child: child, + child: FlowyHover( + style: HoverStyle( + hoverColor: hoverColor, + foregroundColorOnHover: + iconColorOnHover ?? Theme.of(context).iconTheme.color, + backgroundColor: fillColor ?? Colors.transparent, + ), + child: Padding( + padding: iconPadding, + child: SizedBox.fromSize(size: childSize, child: child), + ), + ), ), ), ); @@ -105,16 +95,18 @@ class FlowyIconButton extends StatelessWidget { } class FlowyDropdownButton extends StatelessWidget { - const FlowyDropdownButton({super.key, this.onPressed}); - final VoidCallback? onPressed; + const FlowyDropdownButton({ + Key? key, + this.onPressed, + }) : super(key: key); @override Widget build(BuildContext context) { return FlowyIconButton( width: 16, onPressed: onPressed, - icon: const FlowySvg(FlowySvgData("home/drop_down_show")), + icon: svgWidget("home/drop_down_show"), ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart index f0c979b5cf..6c3810fb7b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart @@ -6,7 +6,8 @@ class FlowyImageIcon extends StatelessWidget { final Color? color; final double? size; - const FlowyImageIcon(this.image, {super.key, this.color, this.size}); + const FlowyImageIcon(this.image, {Key? key, this.color, this.size}) + : super(key: key); @override Widget build(BuildContext context) { 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/progress_indicator.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart index ccf6608dd4..8eb2a12047 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart @@ -13,7 +13,7 @@ List _kDefaultRainbowColors = const [ // CircularProgressIndicator() class FlowyProgressIndicator extends StatelessWidget { - const FlowyProgressIndicator({super.key}); + const FlowyProgressIndicator({Key? key}) : super(key: key); @override Widget build(BuildContext context) { 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..196d0e1848 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 @@ -18,7 +18,7 @@ class StyledListView extends StatefulWidget { final IndexedWidgetBuilder itemBuilder; StyledListView({ - super.key, + Key? key, required this.itemBuilder, required this.itemCount, this.itemExtent, @@ -26,7 +26,7 @@ class StyledListView extends StatefulWidget { this.padding, this.barSize, this.scrollbarPadding, - }) { + }) : super(key: key) { assert(itemExtent != 0, 'Item extent should never be 0, null is ok.'); } @@ -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); @@ -51,7 +66,7 @@ class StyledListViewState extends State { contentSize: contentSize, axis: widget.axis, controller: scrollController, - barSize: widget.barSize ?? 8, + barSize: widget.barSize ?? 12, scrollbarPadding: widget.scrollbarPadding, child: ListView.builder( padding: widget.padding, @@ -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..595991b5c4 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,27 +1,13 @@ -import 'dart:async'; import 'dart:math'; - +import 'dart:async'; import 'package:async/async.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/size.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 { - const StyledScrollbar({ - super.key, - this.size, - required this.axis, - required this.controller, - this.onDrag, - this.contentSize, - this.showTrack = false, - this.autoHideScrollbar = true, - this.handleColor, - this.trackColor, - }); - final double? size; final Axis axis; final ScrollController controller; @@ -36,6 +22,19 @@ class StyledScrollbar extends StatefulWidget { // https://stackoverflow.com/questions/60855712/flutter-how-to-force-scrollcontroller-to-recalculate-position-maxextents final double? contentSize; + const StyledScrollbar( + {Key? key, + this.size, + required this.axis, + required this.controller, + this.onDrag, + this.contentSize, + this.showTrack = false, + this.autoHideScrollbar = true, + this.handleColor, + this.trackColor}) + : super(key: key); + @override ScrollbarState createState() => ScrollbarState(); } @@ -48,29 +47,25 @@ class ScrollbarState extends State { @override void initState() { super.initState(); - widget.controller.addListener(_onScrollChanged); - widget.controller.position.isScrollingNotifier - .addListener(_hideScrollbarInTime); + widget.controller.addListener(() => setState(() {})); + + widget.controller.position.isScrollingNotifier.addListener( + _hideScrollbarInTime, + ); } @override - void dispose() { - if (widget.controller.hasClients) { - widget.controller.removeListener(_onScrollChanged); - widget.controller.position.isScrollingNotifier - .removeListener(_hideScrollbarInTime); - } - super.dispose(); + void didUpdateWidget(StyledScrollbar oldWidget) { + if (oldWidget.contentSize != widget.contentSize) setState(() {}); + super.didUpdateWidget(oldWidget); } - void _onScrollChanged() => setState(() {}); - @override Widget build(BuildContext context) { return LayoutBuilder( builder: (_, BoxConstraints constraints) { double maxExtent; - final double? contentSize = widget.contentSize; + final contentSize = widget.contentSize; switch (widget.axis) { case Axis.vertical: @@ -115,15 +110,22 @@ class ScrollbarState extends State { } // Hide the handle if content is < the viewExtent - var showHandle = hideHandler - ? false - : contentExtent > _viewExtent && contentExtent > 0; + var showHandle = contentExtent > _viewExtent && contentExtent > 0; - // Track color - var trackColor = widget.trackColor ?? + if (hideHandler) { + showHandle = false; + } + + // 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 + ? AFThemeExtension.of(context).greyHover.withOpacity(.1) + : AFThemeExtension.of(context).greyHover.withOpacity(.3)); // Layout the stack, it just contains a child, and return Stack( @@ -155,24 +157,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, + ), + ), ), ), ) @@ -189,7 +185,7 @@ class ScrollbarState extends State { if (!widget.controller.position.isScrollingNotifier.value) { _hideScrollbarOperation = CancelableOperation.fromFuture( - Future.delayed(const Duration(seconds: 2)), + Future.delayed(const Duration(seconds: 2), () {}), ).then((_) { hideHandler = true; if (mounted) { @@ -221,6 +217,16 @@ class ScrollbarState extends State { } class ScrollbarListStack extends StatelessWidget { + final double barSize; + final Axis axis; + final Widget child; + final ScrollController controller; + final double? contentSize; + final EdgeInsets? scrollbarPadding; + final Color? handleColor; + final Color? trackColor; + final bool autoHideScrollbar; + const ScrollbarListStack({ super.key, required this.barSize, @@ -232,38 +238,20 @@ class ScrollbarListStack extends StatelessWidget { this.handleColor, this.autoHideScrollbar = true, this.trackColor, - this.showTrack = false, - this.includeInsets = true, }); - final double barSize; - final Axis axis; - final Widget child; - final ScrollController controller; - final double? contentSize; - final EdgeInsets? scrollbarPadding; - final Color? handleColor; - final Color? trackColor; - final bool showTrack; - final bool autoHideScrollbar; - final bool includeInsets; - @override Widget build(BuildContext context) { return Stack( children: [ - /// Wrap with a bit of padding on the right or bottom to make room for the scrollbar - Padding( - padding: !includeInsets - ? EdgeInsets.zero - : EdgeInsets.only( - right: axis == Axis.vertical ? barSize + Insets.m : 0, - bottom: axis == Axis.horizontal ? barSize + Insets.m : 0, - ), - child: child, + /// LIST + /// Wrap with a bit of padding on the right + child.padding( + right: axis == Axis.vertical ? barSize + Insets.sm : 0, + bottom: axis == Axis.horizontal ? barSize + Insets.sm : 0, ), - /// Display the scrollbar + /// SCROLLBAR Padding( padding: scrollbarPadding ?? EdgeInsets.zero, child: StyledScrollbar( @@ -274,10 +262,9 @@ class ScrollbarListStack extends StatelessWidget { trackColor: trackColor, handleColor: handleColor, autoHideScrollbar: autoHideScrollbar, - showTrack: showTrack, ), ) - // The animate will be used by the children that are using styled_widget. + // The animate will be used by the children that using styled_widget. .animate(const Duration(milliseconds: 250), Curves.easeOut), ], ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart index b1b7c8afc7..b57b4059cf 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart @@ -4,21 +4,6 @@ import 'styled_list.dart'; import 'styled_scroll_bar.dart'; class StyledSingleChildScrollView extends StatefulWidget { - const StyledSingleChildScrollView({ - super.key, - required this.child, - this.contentSize, - this.axis = Axis.vertical, - this.trackColor, - this.handleColor, - this.controller, - this.scrollbarPadding, - this.barSize = 8, - this.autoHideScrollbar = true, - this.includeInsets = true, - }); - - final Widget? child; final double? contentSize; final Axis axis; final Color? trackColor; @@ -26,8 +11,20 @@ class StyledSingleChildScrollView extends StatefulWidget { final ScrollController? controller; final EdgeInsets? scrollbarPadding; final double barSize; - final bool autoHideScrollbar; - final bool includeInsets; + + final Widget? child; + + const StyledSingleChildScrollView({ + Key? key, + @required this.child, + this.contentSize, + this.axis = Axis.vertical, + this.trackColor, + this.handleColor, + this.controller, + this.scrollbarPadding, + this.barSize = 12, + }) : super(key: key); @override State createState() => @@ -36,21 +33,31 @@ class StyledSingleChildScrollView extends StatefulWidget { class StyledSingleChildScrollViewState extends State { - late final ScrollController scrollController = - widget.controller ?? ScrollController(); + late ScrollController scrollController; + + @override + void initState() { + scrollController = widget.controller ?? ScrollController(); + super.initState(); + } @override void dispose() { - if (widget.controller == null) { - scrollController.dispose(); - } + // scrollController.dispose(); super.dispose(); } + @override + void didUpdateWidget(StyledSingleChildScrollView oldWidget) { + if (oldWidget.child != widget.child) { + setState(() {}); + } + super.didUpdateWidget(oldWidget); + } + @override Widget build(BuildContext context) { return ScrollbarListStack( - autoHideScrollbar: widget.autoHideScrollbar, contentSize: widget.contentSize, axis: widget.axis, controller: scrollController, @@ -58,7 +65,6 @@ class StyledSingleChildScrollViewState barSize: widget.barSize, trackColor: widget.trackColor, handleColor: widget.handleColor, - includeInsets: widget.includeInsets, child: SingleChildScrollView( scrollDirection: widget.axis, physics: StyledScrollPhysics(), @@ -70,16 +76,6 @@ class StyledSingleChildScrollViewState } class StyledCustomScrollView extends StatefulWidget { - const StyledCustomScrollView({ - super.key, - this.axis = Axis.vertical, - this.trackColor, - this.handleColor, - this.verticalController, - this.slivers = const [], - this.barSize = 8, - }); - final Axis axis; final Color? trackColor; final Color? handleColor; @@ -87,13 +83,42 @@ class StyledCustomScrollView extends StatefulWidget { final List slivers; final double barSize; + const StyledCustomScrollView({ + Key? key, + this.axis = Axis.vertical, + this.trackColor, + this.handleColor, + this.verticalController, + this.slivers = const [], + this.barSize = 12, + }) : super(key: key); + @override StyledCustomScrollViewState createState() => StyledCustomScrollViewState(); } class StyledCustomScrollViewState extends State { - late final ScrollController controller = - widget.verticalController ?? ScrollController(); + late ScrollController controller; + + @override + void initState() { + controller = widget.verticalController ?? ScrollController(); + + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + void didUpdateWidget(StyledCustomScrollView oldWidget) { + if (oldWidget.slivers != widget.slivers) { + setState(() {}); + } + super.didUpdateWidget(oldWidget); + } @override Widget build(BuildContext context) { 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..dfa12313a3 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 @@ -1,27 +1,26 @@ -import 'dart:io'; - -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) { +void showSnapBar(BuildContext context, String title, [Color? backgroundColor]) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context) .showSnackBar( SnackBar( - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, - duration: const Duration(milliseconds: 8000), - content: FlowyText( - title, - maxLines: 2, - fontSize: - (Platform.isLinux || Platform.isWindows || Platform.isMacOS) - ? 14 - : 12, + content: WillPopScope( + onWillPop: () async { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + return true; + }, + child: Text( + title, + style: const TextStyle( + color: Colors.black, + ), + ), ), + backgroundColor: backgroundColor, ), ) .closed - .then((value) => onClosed?.call()); + .then((value) => null); } 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..e90b45fdf8 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,8 +1,4 @@ -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 { final String text; @@ -13,25 +9,11 @@ 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 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, this.overflow = TextOverflow.clip, this.fontSize, this.fontWeight, @@ -39,204 +21,82 @@ 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( - this.text, { - super.key, - this.overflow, - this.color, - this.textAlign, - this.maxLines = 1, - this.decoration, - this.decorationColor, - 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; + Key? key, + }) : super(key: key); const FlowyText.regular( this.text, { - super.key, this.fontSize, this.overflow, this.color, 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; + Key? key, + }) : fontWeight = FontWeight.w400, + super(key: key); const FlowyText.medium( this.text, { - super.key, this.fontSize, this.overflow, this.color, 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; + Key? key, + }) : fontWeight = FontWeight.w500, + super(key: key); const FlowyText.semibold( this.text, { - super.key, this.fontSize, this.overflow, this.color, 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 - const FlowyText.emoji( - this.text, { - super.key, - this.fontSize, - this.overflow, - this.color, - this.textAlign = TextAlign.center, - this.maxLines = 1, - this.decoration, - this.decorationColor, - 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; + Key? key, + }) : fontWeight = FontWeight.w600, + super(key: key); @override Widget build(BuildContext context) { - Widget child; - - var fontFamily = this.fontFamily; - var fallbackFontFamily = this.fallbackFontFamily; - var fontSize = - this.fontSize ?? Theme.of(context).textTheme.bodyMedium!.fontSize!; - if (isEmoji && _useNotoColorEmoji) { - fontFamily = _loadEmojiFontFamilyIfNeeded(); - if (fontFamily != null && fallbackFontFamily == null) { - fallbackFontFamily = [fontFamily]; - } - } - - 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; - } - } - - final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: fontSize, - 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 (withTooltip) { - child = FlowyTooltip( - message: text, - child: child, + if (selectable) { + return SelectableText( + text, + maxLines: maxLines, + textAlign: textAlign, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + decoration: decoration, + fontFamily: fontFamily, + ), + ); + } else { + return Text( + text, + maxLines: maxLines, + textAlign: textAlign, + overflow: overflow ?? TextOverflow.clip, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + decoration: decoration, + fontFamily: fontFamily, + ), ); } - - return child; } - - String? _loadEmojiFontFamilyIfNeeded() { - if (_useNotoColorEmoji) { - return GoogleFonts.notoColorEmoji().fontFamily; - } - - return null; - } - - bool get _useNotoColorEmoji => Platform.isLinux; } 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..1a4a89aa8c 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 @@ -5,9 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class FlowyTextField extends StatefulWidget { - final String? hintText; - final String? text; - final TextStyle? textStyle; + final String hintText; + final String text; final void Function(String)? onChanged; final void Function()? onEditingComplete; final void Function(String)? onSubmitted; @@ -20,33 +19,11 @@ class FlowyTextField extends StatefulWidget { final bool submitOnLeave; final Duration? debounceDuration; final String? errorText; - final Widget? error; - final int? maxLines; - final bool showCounter; - final Widget? prefixIcon; - final Widget? suffixIcon; - final BoxConstraints? prefixIconConstraints; - final BoxConstraints? suffixIconConstraints; - final BoxConstraints? hintTextConstraints; - final TextStyle? hintStyle; - final InputDecoration? decoration; - final TextAlignVertical? textAlignVertical; - final TextInputAction? textInputAction; - final TextInputType? keyboardType; - 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; + final int maxLines; const FlowyTextField({ - super.key, this.hintText = "", - this.text, - this.textStyle, + this.text = "", this.onChanged, this.onEditingComplete, this.onSubmitted, @@ -59,28 +36,9 @@ class FlowyTextField extends StatefulWidget { this.submitOnLeave = false, this.debounceDuration, this.errorText, - this.error, this.maxLines = 1, - this.showCounter = true, - this.prefixIcon, - this.suffixIcon, - this.prefixIconConstraints, - this.suffixIconConstraints, - this.hintTextConstraints, - this.hintStyle, - this.decoration, - this.textAlignVertical, - this.textInputAction, - this.keyboardType = TextInputType.multiline, - this.inputFormatters, - this.obscureText = false, - this.isDense = true, - this.readOnly = false, - this.enableBorderColor, - this.borderRadius, - this.onTap, - this.onTapOutside, - }); + Key? key, + }) : super(key: key); @override State createState() => FlowyTextFieldState(); @@ -93,40 +51,21 @@ class FlowyTextFieldState extends State { @override void initState() { - super.initState(); - focusNode = widget.focusNode ?? FocusNode(); focusNode.addListener(notifyDidEndEditing); - controller = widget.controller ?? TextEditingController(); - - if (widget.text != null) { - controller.text = widget.text!; + if (widget.controller != null) { + controller = widget.controller!; + } else { + controller = TextEditingController(); + controller.text = widget.text; } - if (widget.autoFocus) { WidgetsBinding.instance.addPostFrameCallback((_) { focusNode.requestFocus(); - if (widget.controller == null) { - controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length), - ); - } }); } - } - - @override - void dispose() { - focusNode.removeListener(notifyDidEndEditing); - if (widget.focusNode == null) { - focusNode.dispose(); - } - if (widget.controller == null) { - controller.dispose(); - } - _debounceOnChanged?.cancel(); - super.dispose(); + super.initState(); } void _debounceOnChangedText(Duration duration, String text) { @@ -146,14 +85,14 @@ class FlowyTextFieldState extends State { void _onSubmitted(String text) { widget.onSubmitted?.call(text); if (widget.autoClearWhenDone) { - controller.clear(); + controller.text = ""; + setState(() {}); } } @override Widget build(BuildContext context) { return TextField( - readOnly: widget.readOnly, controller: controller, focusNode: focusNode, onChanged: (text) { @@ -163,81 +102,63 @@ 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, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, - style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall, - textAlignVertical: widget.textAlignVertical ?? TextAlignVertical.center, - keyboardType: widget.keyboardType, - inputFormatters: widget.inputFormatters, - obscureText: widget.obscureText, - decoration: widget.decoration ?? - InputDecoration( - constraints: widget.hintTextConstraints ?? - BoxConstraints( - maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58, - ), - contentPadding: EdgeInsets.symmetric( - horizontal: widget.isDense ? 12 : 18, - vertical: - (widget.maxLines == null || widget.maxLines! > 1) ? 12 : 0, - ), - enabledBorder: OutlineInputBorder( - borderRadius: widget.borderRadius ?? Corners.s8Border, - borderSide: BorderSide( - color: widget.enableBorderColor ?? - Theme.of(context).colorScheme.outline, - ), - ), - isDense: false, - hintText: widget.hintText, - errorText: widget.errorText, - error: widget.error, - errorStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.error), - hintStyle: widget.hintStyle ?? - Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).hintColor), - suffixText: widget.showCounter ? _suffixText() : "", - counterText: "", - focusedBorder: OutlineInputBorder( - borderRadius: widget.borderRadius ?? Corners.s8Border, - borderSide: BorderSide( - color: widget.readOnly - ? widget.enableBorderColor ?? - Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - ), - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: widget.borderRadius ?? Corners.s8Border, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: widget.borderRadius ?? Corners.s8Border, - ), - prefixIcon: widget.prefixIcon, - suffixIcon: widget.suffixIcon, - prefixIconConstraints: widget.prefixIconConstraints, - suffixIconConstraints: widget.suffixIconConstraints, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 13), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + width: 1.0, ), + borderRadius: Corners.s10Border, + ), + isDense: true, + hintText: widget.hintText, + errorText: widget.errorText, + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).hintColor), + suffixText: _suffixText(), + counterText: "", + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 1.0, + ), + borderRadius: Corners.s10Border, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 1.0, + ), + borderRadius: Corners.s10Border, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 1.0, + ), + borderRadius: Corners.s10Border, + ), + ), ); } + @override + void dispose() { + focusNode.removeListener(notifyDidEndEditing); + focusNode.dispose(); + super.dispose(); + } + void notifyDidEndEditing() { if (!focusNode.hasFocus) { if (controller.text.isNotEmpty && widget.submitOnLeave) { @@ -251,7 +172,8 @@ class FlowyTextFieldState extends State { String? _suffixText() { if (widget.maxLength != null) { return ' ${controller.text.length}/${widget.maxLength}'; + } else { + return null; } - return null; } } 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..f7db45d8b2 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 @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:math' as math; - import 'package:flowy_infra/size.dart'; 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; @@ -16,8 +16,6 @@ class FlowyFormTextInput extends StatelessWidget { final TextStyle? textStyle; final TextAlign textAlign; final int? maxLines; - final int? maxLength; - final bool showCounter; final TextEditingController? controller; final TextCapitalization? capitalization; final Function(String)? onChanged; @@ -25,25 +23,23 @@ class FlowyFormTextInput extends StatelessWidget { final Function(bool)? onFocusChanged; final Function(FocusNode)? onFocusCreated; - const FlowyFormTextInput({ - super.key, - this.label, - this.autoFocus, - this.initialValue, - this.onChanged, - this.onEditingComplete, - this.hintText, - this.onFocusChanged, - this.onFocusCreated, - this.controller, - this.contentPadding, - this.capitalization, - this.textStyle, - this.textAlign = TextAlign.center, - this.maxLines, - this.maxLength, - this.showCounter = true, - }); + const FlowyFormTextInput( + {Key? key, + this.label, + this.autoFocus, + this.initialValue, + this.onChanged, + this.onEditingComplete, + this.hintText, + this.onFocusChanged, + this.onFocusCreated, + this.controller, + this.contentPadding, + this.capitalization, + this.textStyle, + this.textAlign = TextAlign.center, + this.maxLines}) + : super(key: key); @override Widget build(BuildContext context) { @@ -60,17 +56,13 @@ class FlowyFormTextInput extends StatelessWidget { onFocusChanged: onFocusChanged, controller: controller, maxLines: maxLines, - maxLength: maxLength, - showCounter: showCounter, - contentPadding: contentPadding ?? kDefaultTextInputPadding, - hintText: hintText, - hintStyle: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).hintColor.withValues(alpha: 0.7)), - isDense: true, - inputBorder: const ThinUnderlineBorder( - borderSide: BorderSide(width: 5, color: Colors.red), + inputDecoration: InputDecoration( + isDense: true, + contentPadding: contentPadding ?? kDefaultTextInputPadding, + border: const ThinUnderlineBorder( + borderSide: BorderSide(width: 5, color: Colors.red)), + //focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: .5, color: Colors.red)), + hintText: hintText, ), ); } @@ -86,8 +78,6 @@ class StyledSearchTextInput extends StatefulWidget { final IconData? icon; final String? initialValue; final int? maxLines; - final int? maxLength; - final bool showCounter; final TextEditingController? controller; final TextCapitalization? capitalization; final TextInputType? type; @@ -95,14 +85,11 @@ class StyledSearchTextInput extends StatefulWidget { final bool? autoValidate; final bool? enableSuggestions; final bool? autoCorrect; - final bool isDense; final String? errorText; final String? hintText; - final TextStyle? hintStyle; final Widget? prefixIcon; final Widget? suffixIcon; final InputDecoration? inputDecoration; - final InputBorder? inputBorder; final Function(String)? onChanged; final Function()? onEditingComplete; @@ -114,7 +101,7 @@ class StyledSearchTextInput extends StatefulWidget { final VoidCallback? onTap; const StyledSearchTextInput({ - super.key, + Key? key, this.label, this.autoFocus = false, this.obscureText = false, @@ -127,7 +114,6 @@ class StyledSearchTextInput extends StatefulWidget { this.autoValidate = false, this.enableSuggestions = true, this.autoCorrect = true, - this.isDense = false, this.errorText, this.style, this.contentPadding, @@ -143,13 +129,9 @@ class StyledSearchTextInput extends StatefulWidget { this.onSaved, this.onTap, this.hintText, - this.hintStyle, this.capitalization, this.maxLines, - this.maxLength, - this.showCounter = false, - this.inputBorder, - }); + }) : super(key: key); @override StyledSearchTextInputState createState() => StyledSearchTextInputState(); @@ -161,36 +143,35 @@ class StyledSearchTextInputState extends State { @override void initState() { - super.initState(); _controller = widget.controller ?? TextEditingController(text: widget.initialValue); _focusNode = FocusNode( - debugLabel: widget.label, - canRequestFocus: true, - onKeyEvent: (node, event) { - if (event.logicalKey == LogicalKeyboardKey.escape) { - widget.onEditingCancel?.call(); - return KeyEventResult.handled; + debugLabel: widget.label ?? '', + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt is RawKeyDownEvent) { + if (evt.logicalKey == LogicalKeyboardKey.escape) { + widget.onEditingCancel?.call(); + return KeyEventResult.handled; + } } + 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); + _controller.dispose(); _focusNode.dispose(); super.dispose(); } @@ -223,40 +204,28 @@ class StyledSearchTextInputState extends State { showCursor: true, enabled: widget.enabled, maxLines: widget.maxLines, - maxLength: widget.maxLength, textCapitalization: widget.capitalization ?? TextCapitalization.none, textAlign: widget.textAlign, decoration: widget.inputDecoration ?? InputDecoration( prefixIcon: widget.prefixIcon, suffixIcon: widget.suffixIcon, - counterText: "", - suffixText: widget.showCounter ? _suffixText() : "", contentPadding: widget.contentPadding ?? EdgeInsets.all(Insets.m), - border: widget.inputBorder ?? - const OutlineInputBorder(borderSide: BorderSide.none), - isDense: widget.isDense, + border: const OutlineInputBorder(borderSide: BorderSide.none), + isDense: true, icon: widget.icon == null ? null : Icon(widget.icon), errorText: widget.errorText, errorMaxLines: 2, hintText: widget.hintText, - hintStyle: widget.hintStyle ?? - Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).hintColor), + hintStyle: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), labelText: widget.label, ), ), ); } - - String? _suffixText() { - if (widget.controller != null && widget.maxLength != null) { - return ' ${widget.controller!.text.length}/${widget.maxLength}'; - } - return null; - } } class ThinUnderlineBorder extends InputBorder { @@ -271,12 +240,12 @@ class ThinUnderlineBorder extends InputBorder { /// and right corners have a circular radius of 4.0. The [borderRadius] /// parameter must not be null. const ThinUnderlineBorder({ - super.borderSide = const BorderSide(), + BorderSide borderSide = const BorderSide(), this.borderRadius = const BorderRadius.only( topLeft: Radius.circular(4.0), topRight: Radius.circular(4.0), ), - }); + }) : super(borderSide: borderSide); /// The radii of the border's rounded rectangle corners. /// @@ -293,10 +262,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 +271,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..b9c5894b3a 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 @@ -22,7 +22,7 @@ class BaseStyledButton extends StatefulWidget { final Color outlineColor; const BaseStyledButton({ - super.key, + Key? key, required this.child, this.onPressed, this.onFocusChanged, @@ -39,7 +39,7 @@ class BaseStyledButton extends StatefulWidget { this.useBtnText = true, this.autoFocus = false, this.outlineColor = Colors.transparent, - }); + }) : super(key: key); @override State createState() => BaseStyledBtnState(); @@ -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..e67a7a15df 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 @@ -1,21 +1,21 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; - +import 'package:flowy_infra/size.dart'; import 'base_styled_button.dart'; -import 'secondary_button.dart'; class PrimaryTextButton extends StatelessWidget { final String label; final VoidCallback? onPressed; - final TextButtonMode mode; + final bool bigMode; const PrimaryTextButton(this.label, - {super.key, this.onPressed, this.mode = TextButtonMode.big}); + {Key? key, this.onPressed, this.bigMode = false}) + : super(key: key); @override Widget build(BuildContext context) { return PrimaryButton( - mode: mode, + bigMode: bigMode, onPressed: onPressed, child: FlowyText.regular( label, @@ -26,28 +26,23 @@ class PrimaryTextButton extends StatelessWidget { } class PrimaryButton extends StatelessWidget { - const PrimaryButton({ - super.key, - required this.child, - this.onPressed, - this.mode = TextButtonMode.big, - this.backgroundColor, - }); - final Widget child; final VoidCallback? onPressed; - final TextButtonMode mode; - final Color? backgroundColor; + final bool bigMode; + + const PrimaryButton( + {Key? key, required this.child, this.onPressed, this.bigMode = false}) + : super(key: key); @override Widget build(BuildContext context) { return BaseStyledButton( - minWidth: mode.size.width, - minHeight: mode.size.height, - contentPadding: const EdgeInsets.symmetric(horizontal: 6), - bgColor: backgroundColor ?? Theme.of(context).colorScheme.primary, + minWidth: bigMode ? 100 : 80, + minHeight: bigMode ? 40 : 38, + contentPadding: EdgeInsets.zero, + bgColor: Theme.of(context).colorScheme.primary, hoverColor: Theme.of(context).colorScheme.primaryContainer, - borderRadius: mode.borderRadius, + borderRadius: bigMode ? Corners.s12Border : Corners.s8Border, onPressed: onPressed, child: child, ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart index def795ed44..d45e6affbd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart @@ -4,29 +4,29 @@ import 'package:flowy_infra/size.dart'; import 'base_styled_button.dart'; -enum TextButtonMode { +enum SecondaryTextButtonMode { normal, big, small; Size get size { switch (this) { - case TextButtonMode.normal: - return const Size(80, 32); - case TextButtonMode.big: + case SecondaryTextButtonMode.normal: + return const Size(80, 38); + case SecondaryTextButtonMode.big: return const Size(100, 40); - case TextButtonMode.small: + case SecondaryTextButtonMode.small: return const Size(100, 30); } } BorderRadius get borderRadius { switch (this) { - case TextButtonMode.normal: + case SecondaryTextButtonMode.normal: return Corners.s8Border; - case TextButtonMode.big: + case SecondaryTextButtonMode.big: return Corners.s12Border; - case TextButtonMode.small: + case SecondaryTextButtonMode.small: return Corners.s6Border; } } @@ -37,26 +37,21 @@ class SecondaryTextButton extends StatelessWidget { this.label, { super.key, this.onPressed, - this.textColor, - this.outlineColor, - this.mode = TextButtonMode.normal, + this.mode = SecondaryTextButtonMode.normal, }); final String label; final VoidCallback? onPressed; - final TextButtonMode mode; - final Color? textColor; - final Color? outlineColor; + final SecondaryTextButtonMode mode; @override Widget build(BuildContext context) { return SecondaryButton( mode: mode, onPressed: onPressed, - outlineColor: outlineColor, child: FlowyText.regular( label, - color: textColor ?? Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.primary, ), ); } @@ -67,14 +62,12 @@ class SecondaryButton extends StatelessWidget { super.key, required this.child, this.onPressed, - this.outlineColor, - this.mode = TextButtonMode.normal, + this.mode = SecondaryTextButtonMode.normal, }); final Widget child; final VoidCallback? onPressed; - final TextButtonMode mode; - final Color? outlineColor; + final SecondaryTextButtonMode mode; @override Widget build(BuildContext context) { @@ -83,8 +76,8 @@ class SecondaryButton extends StatelessWidget { minWidth: size.width, minHeight: size.height, contentPadding: EdgeInsets.zero, - bgColor: Colors.transparent, - outlineColor: outlineColor ?? Theme.of(context).colorScheme.primary, + bgColor: Theme.of(context).colorScheme.surface, + outlineColor: Theme.of(context).colorScheme.primary, borderRadius: mode.borderRadius, onPressed: onPressed, child: child, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/clickable_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/clickable_extension.dart new file mode 100644 index 0000000000..267913fe9a --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/clickable_extension.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +extension ClickableExtensions on Widget { + Widget clickable(void Function() action, {bool opaque = true}) { + return GestureDetector( + behavior: opaque ? HitTestBehavior.opaque : HitTestBehavior.deferToChild, + onTap: action, + child: MouseRegion( + cursor: SystemMouseCursors.click, + opaque: opaque, + child: this, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart index 63a5dd5f60..8fb60fda1d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart @@ -7,10 +7,11 @@ class ConstrainedFlexView extends StatelessWidget { final EdgeInsets scrollPadding; const ConstrainedFlexView(this.minSize, - {super.key, + {Key? key, required this.child, this.axis = Axis.horizontal, - this.scrollPadding = EdgeInsets.zero}); + this.scrollPadding = EdgeInsets.zero}) + : super(key: key); bool get isHz => axis == Axis.horizontal; 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..f853c65e69 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 @@ -1,20 +1,18 @@ -import 'dart:ui'; - +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/widget/dialog/dialog_size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; -import 'package:flowy_infra_ui/widget/dialog/dialog_size.dart'; +import 'dart:ui'; extension IntoDialog on Widget { Future show(BuildContext context) async { FocusNode dialogFocusNode = FocusNode(); await Dialogs.show( - child: KeyboardListener( + RawKeyboardListener( focusNode: dialogFocusNode, - onKeyEvent: (event) { - if (event.logicalKey == LogicalKeyboardKey.escape) { + onKey: (value) { + if (value.isKeyPressed(LogicalKeyboardKey.escape)) { Navigator.of(context).pop(); } }, @@ -37,7 +35,7 @@ class StyledDialog extends StatelessWidget { final bool shrinkWrap; const StyledDialog({ - super.key, + Key? key, required this.child, this.maxWidth, this.maxHeight, @@ -46,7 +44,7 @@ class StyledDialog extends StatelessWidget { this.bgColor, this.borderRadius = const BorderRadius.all(Radius.circular(6)), this.shrinkWrap = true, - }); + }) : super(key: key); @override Widget build(BuildContext context) { @@ -58,24 +56,22 @@ class StyledDialog extends StatelessWidget { ); if (shrinkWrap) { - innerContent = IntrinsicWidth( - child: IntrinsicHeight( - child: innerContent, - )); + innerContent = + IntrinsicWidth(child: IntrinsicHeight(child: innerContent)); } return FocusTraversalGroup( child: Container( margin: margin ?? EdgeInsets.all(Insets.sm * 2), alignment: Alignment.center, - child: ConstrainedBox( + child: Container( constraints: BoxConstraints( minWidth: DialogSize.minDialogWidth, maxHeight: maxHeight ?? double.infinity, maxWidth: maxWidth ?? double.infinity, ), child: ClipRRect( - borderRadius: borderRadius ?? BorderRadius.zero, + borderRadius: borderRadius, child: SingleChildScrollView( physics: StyledScrollPhysics(), //https://medium.com/saugo360/https-medium-com-saugo360-flutter-using-overlay-to-display-floating-widgets-2e6d0e8decb9 @@ -92,11 +88,10 @@ class StyledDialog extends StatelessWidget { } class Dialogs { - static Future show(BuildContext context, - {required Widget child}) async { + static Future show(Widget child, BuildContext context) 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); @@ -110,14 +105,13 @@ class DialogBarrier { String label; Color color; bool dismissible; - ImageFilter? filter; + ImageFilter filter; DialogBarrier({ this.dismissible = true, this.color = Colors.transparent, this.label = '', - this.filter, - }); + }) : filter = ImageFilter.blur(sigmaX: 4, sigmaY: 4); } class StyledDialogRoute extends PopupRoute { @@ -129,11 +123,11 @@ class StyledDialogRoute extends PopupRoute { required this.barrier, Duration transitionDuration = const Duration(milliseconds: 300), RouteTransitionsBuilder? transitionBuilder, - super.settings, + RouteSettings? settings, }) : _pageBuilder = pageBuilder, _transitionDuration = transitionDuration, _transitionBuilder = transitionBuilder, - super(filter: barrier.filter); + super(settings: settings, filter: barrier.filter); @override bool get barrierDismissible { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart new file mode 100644 index 0000000000..a62daf3632 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart @@ -0,0 +1,191 @@ +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class FlowyErrorPage extends StatelessWidget { + factory FlowyErrorPage.error( + Error e, { + required String howToFix, + Key? key, + }) => + FlowyErrorPage._( + e.toString(), + stackTrace: e.stackTrace?.toString(), + howToFix: howToFix, + key: key, + ); + + factory FlowyErrorPage.message( + String message, { + required String howToFix, + String? stackTrace, + Key? key, + }) => + FlowyErrorPage._( + message, + key: key, + stackTrace: stackTrace, + howToFix: howToFix, + ); + + factory FlowyErrorPage.exception( + Exception e, { + required String howToFix, + String? stackTrace, + Key? key, + }) => + FlowyErrorPage._( + e.toString(), + stackTrace: stackTrace, + key: key, + howToFix: howToFix, + ); + + const FlowyErrorPage._( + this.message, { + required this.howToFix, + this.stackTrace, + super.key, + }); + + static const _titleFontSize = 24.0; + static const _titleToMessagePadding = 8.0; + + final String message; + final String? stackTrace; + final String howToFix; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const FlowyText.medium( + "AppFlowy Error", + fontSize: _titleFontSize, + ), + const SizedBox( + height: _titleToMessagePadding, + ), + FlowyText.semibold( + message, + ), + const SizedBox( + height: _titleToMessagePadding, + ), + FlowyText.regular( + howToFix, + ), + const SizedBox( + height: _titleToMessagePadding, + ), + const GitHubRedirectButton(), + const SizedBox( + height: _titleToMessagePadding, + ), + if (stackTrace != null) StackTracePreview(stackTrace!), + ], + ), + ); + } +} + +class StackTracePreview extends StatelessWidget { + const StackTracePreview( + this.stackTrace, { + super.key, + }); + + final String stackTrace; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 350, + maxWidth: 450, + ), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Align( + alignment: Alignment.centerLeft, + child: FlowyText.semibold( + "Stack Trace", + ), + ), + Container( + height: 120, + padding: const EdgeInsets.symmetric(vertical: 8), + child: SingleChildScrollView( + child: Text( + stackTrace, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: FlowyButton( + hoverColor: Theme.of(context).colorScheme.onBackground, + text: const FlowyText( + "Copy", + ), + useIntrinsicWidth: true, + onTap: () => Clipboard.setData( + ClipboardData(text: stackTrace), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class GitHubRedirectButton extends StatelessWidget { + const GitHubRedirectButton({super.key}); + + static const _height = 32.0; + + Uri get _gitHubNewBugUri => Uri( + scheme: 'https', + host: 'github.com', + path: '/AppFlowy-IO/AppFlowy/issues/new', + query: + 'assignees=&labels=&projects=&template=bug_report.yaml&title=%5BBug%5D+', + ); + + @override + Widget build(BuildContext context) { + return FlowyButton( + leftIconSize: const Size.square(_height), + text: const FlowyText( + "AppFlowy", + ), + useIntrinsicWidth: true, + leftIcon: Padding( + padding: const EdgeInsets.all(4.0), + child: svgWidget('login/github-mark'), + ), + onTap: () async { + 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 deleted file mode 100644 index 5b0b791c6c..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:flutter/material.dart'; - -const _tooltipWaitDuration = Duration(milliseconds: 300); - -class FlowyTooltip extends StatelessWidget { - const FlowyTooltip({ - super.key, - this.message, - this.richMessage, - this.preferBelow, - this.margin, - this.verticalOffset, - this.padding, - this.child, - }); - - final String? message; - final InlineSpan? richMessage; - final bool? preferBelow; - final EdgeInsetsGeometry? margin; - final Widget? child; - final double? verticalOffset; - final EdgeInsets? padding; - - @override - Widget build(BuildContext context) { - if (message == null && richMessage == null) { - return child ?? const SizedBox.shrink(); - } - - return Tooltip( - margin: margin, - verticalOffset: verticalOffset ?? 16.0, - padding: padding ?? - const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - decoration: BoxDecoration( - color: context.tooltipBackgroundColor(), - borderRadius: BorderRadius.circular(10.0), - ), - waitDuration: _tooltipWaitDuration, - message: message, - textStyle: message != null ? context.tooltipTextStyle() : null, - richMessage: richMessage, - 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/ignore_parent_gesture.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart index 664d5cd492..9a679775b4 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; class IgnoreParentGestureWidget extends StatelessWidget { const IgnoreParentGestureWidget({ - super.key, + Key? key, required this.child, this.onPress, - }); + }) : super(key: key); final Widget child; final VoidCallback? onPress; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart index dab9a4640d..81529ba16c 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart @@ -6,7 +6,8 @@ class MouseHoverBuilder extends StatefulWidget { final bool isClickable; const MouseHoverBuilder( - {super.key, required this.builder, this.isClickable = false}); + {Key? key, required this.builder, this.isClickable = false}) + : super(key: key); final HoverBuilder builder; 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..a329074d43 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,11 +13,9 @@ class RoundedTextButton extends StatelessWidget { final Color? hoverColor; final Color? textColor; final double? fontSize; - final FontWeight? fontWeight; - final EdgeInsets padding; const RoundedTextButton({ - super.key, + Key? key, this.onPressed, this.title, this.width, @@ -28,9 +26,7 @@ class RoundedTextButton extends StatelessWidget { this.hoverColor, this.textColor, this.fontSize, - this.fontWeight, - this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - }); + }) : super(key: key); @override Widget build(BuildContext context) { @@ -44,7 +40,6 @@ class RoundedTextButton extends StatelessWidget { child: SizedBox.expand( child: FlowyTextButton( title ?? '', - fontWeight: fontWeight, onPressed: onPressed, fontSize: fontSize, mainAxisAlignment: MainAxisAlignment.center, @@ -53,7 +48,6 @@ class RoundedTextButton extends StatelessWidget { fillColor: fillColor ?? Theme.of(context).colorScheme.primary, hoverColor: hoverColor ?? Theme.of(context).colorScheme.primaryContainer, - padding: padding, ), ), ); @@ -69,14 +63,14 @@ class RoundedImageButton extends StatelessWidget { final Widget child; const RoundedImageButton({ - super.key, + Key? key, this.press, required this.size, this.borderRadius = BorderRadius.zero, this.borderColor = Colors.transparent, this.color = Colors.transparent, required this.child, - }); + }) : super(key: key); @override Widget build(BuildContext context) { @@ -86,7 +80,7 @@ class RoundedImageButton extends StatelessWidget { child: TextButton( onPressed: press, style: ButtonStyle( - shape: WidgetStateProperty.all( + shape: MaterialStateProperty.all( RoundedRectangleBorder(borderRadius: borderRadius))), child: child, ), 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..27949fb100 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; @@ -30,7 +29,7 @@ class RoundedInputField extends StatefulWidget { final Function(String)? onFieldSubmitted; const RoundedInputField({ - super.key, + Key? key, this.hintText, this.errorText = "", this.initialValue, @@ -53,7 +52,7 @@ class RoundedInputField extends StatefulWidget { this.autoFocus = false, this.maxLength, this.onFieldSubmitted, - }); + }) : super(key: key); @override State createState() => _RoundedInputFieldState(); @@ -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/lib/widget/route/animation.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart index 257c7c0cf6..e0f328afc9 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart @@ -8,10 +8,9 @@ class PageRoutes { static const Curve kDefaultEaseFwd = Curves.easeOut; static const Curve kDefaultEaseReverse = Curves.easeOut; - static Route fade(PageBuilder pageBuilder, RouteSettings? settings, + static Route fade(PageBuilder pageBuilder, [double duration = kDefaultDuration]) { return PageRouteBuilder( - settings: settings, transitionDuration: Duration(milliseconds: (duration * 1000).round()), pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(), transitionsBuilder: (context, animation, secondaryAnimation, child) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/separated_flex.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/separated_flex.dart deleted file mode 100644 index c59c15e73a..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/separated_flex.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; - -typedef SeparatorBuilder = Widget Function(); - -Widget _defaultColumnSeparatorBuilder() => const Divider(); -Widget _defaultRowSeparatorBuilder() => const VerticalDivider(); - -class SeparatedColumn extends Column { - SeparatedColumn({ - super.key, - super.mainAxisAlignment, - super.crossAxisAlignment, - super.mainAxisSize, - super.textBaseline, - super.textDirection, - super.verticalDirection, - SeparatorBuilder separatorBuilder = _defaultColumnSeparatorBuilder, - required List children, - }) : super(children: _insertSeparators(children, separatorBuilder)); -} - -class SeparatedRow extends Row { - SeparatedRow({ - super.key, - super.mainAxisAlignment, - super.crossAxisAlignment, - super.mainAxisSize, - super.textBaseline, - super.textDirection, - super.verticalDirection, - SeparatorBuilder separatorBuilder = _defaultRowSeparatorBuilder, - required List children, - }) : super(children: _insertSeparators(children, separatorBuilder)); -} - -List _insertSeparators( - List children, - SeparatorBuilder separatorBuilder, -) { - if (children.length < 2) { - return children; - } - - List newChildren = []; - for (int i = 0; i < children.length - 1; i++) { - newChildren.add(children[i]); - newChildren.add(separatorBuilder()); - } - return newChildren..add(children.last); -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/seperated_column.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/seperated_column.dart new file mode 100644 index 0000000000..7362f989e5 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/seperated_column.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +typedef SeparatorBuilder = Widget Function(); + +class SeparatedColumn extends StatelessWidget { + final List children; + final SeparatorBuilder? separatorBuilder; + final MainAxisAlignment mainAxisAlignment; + final CrossAxisAlignment crossAxisAlignment; + final MainAxisSize mainAxisSize; + final TextBaseline? textBaseline; + final TextDirection? textDirection; + final VerticalDirection verticalDirection; + + const SeparatedColumn({ + Key? key, + required this.children, + this.separatorBuilder, + this.mainAxisAlignment = MainAxisAlignment.start, + this.crossAxisAlignment = CrossAxisAlignment.center, + this.mainAxisSize = MainAxisSize.max, + this.verticalDirection = VerticalDirection.down, + this.textBaseline, + this.textDirection, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var c = children.toList(); + for (var i = c.length; i-- > 0;) { + if (i > 0 && separatorBuilder != null) c.insert(i, separatorBuilder!()); + } + return Column( + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + mainAxisSize: mainAxisSize, + textBaseline: textBaseline, + textDirection: textDirection, + verticalDirection: verticalDirection, + children: c, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart index 1abb5b8fdb..e2f9046015 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart @@ -4,60 +4,26 @@ class Space extends StatelessWidget { final double width; final double height; - const Space(this.width, this.height, {super.key}); + const Space(this.width, this.height, {Key? key}) : super(key: key); @override Widget build(BuildContext context) => SizedBox(width: width, height: height); } class VSpace extends StatelessWidget { - const VSpace( - this.size, { - super.key, - this.color, - }); - final double size; - final Color? color; + + const VSpace(this.size, {Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - if (color != null) { - return SizedBox( - height: size, - width: double.infinity, - child: ColoredBox( - color: color!, - ), - ); - } else { - return Space(0, size); - } - } + Widget build(BuildContext context) => Space(0, size); } class HSpace extends StatelessWidget { - const HSpace( - this.size, { - super.key, - this.color, - }); - final double size; - final Color? color; + + const HSpace(this.size, {Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - if (color != null) { - return SizedBox( - height: double.infinity, - width: size, - child: ColoredBox( - color: color!, - ), - ); - } else { - return Space(size, 0); - } - } + Widget build(BuildContext context) => Space(size, 0); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/macos/flowy_infra_ui.podspec b/frontend/appflowy_flutter/packages/flowy_infra_ui/macos/flowy_infra_ui.podspec index 83c93b09b1..35b105dbea 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/macos/flowy_infra_ui.podspec +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/macos/flowy_infra_ui.podspec @@ -16,7 +16,7 @@ A new flutter plugin project. s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' - s.platform = :osx, '10.13' + s.platform = :osx, '10.11' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index b5b5c22bc7..69afe449df 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -13,32 +13,29 @@ dependencies: sdk: flutter # Thirdparty packages - + dartz: + provider: ^6.0.5 styled_widget: ^0.4.1 + equatable: ^2.0.5 animations: ^2.0.7 loading_indicator: ^3.1.0 async: url_launcher: ^6.1.11 - google_fonts: ^6.1.0 # 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: path: ../flowy_infra - 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 + flutter_lints: ^2.0.1 flutter: plugin: diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/bug_report.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 50a4c7b8b7..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us improve -title: "fix: " -labels: bug ---- - -**Description** - -A clear and concise description of what the bug is. - -**Steps To Reproduce** - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected Behavior** - -A clear and concise description of what you expected to happen. - -**Screenshots** - -If applicable, add screenshots to help explain your problem. - -**Additional Context** - -Add any other context about the problem here. diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/build.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/build.md deleted file mode 100644 index 0cf8e62cdb..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/build.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Build System -about: Changes that affect the build system or external dependencies -title: "build: " -labels: build ---- - -**Description** - -Describe what changes need to be done to the build system and why. - -**Requirements** - -- [ ] The build system is passing diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/chore.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/chore.md deleted file mode 100644 index 498ebfd821..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/chore.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Chore -about: Other changes that don't modify src or test files -title: "chore: " -labels: chore ---- - -**Description** - -Clearly describe what change is needed and why. If this changes code then please use another issue type. - -**Requirements** - -- [ ] No functional changes to the code diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/ci.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/ci.md deleted file mode 100644 index fa2dd9e2d0..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/ci.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Continuous Integration -about: Changes to the CI configuration files and scripts -title: "ci: " -labels: ci ---- - -**Description** - -Describe what changes need to be done to the ci/cd system and why. - -**Requirements** - -- [ ] The ci system is passing diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/config.yml b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index ec4bb386bc..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: false \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/documentation.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index f494a4d98b..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Documentation -about: Improve the documentation so all collaborators have a common understanding -title: "docs: " -labels: documentation ---- - -**Description** - -Clearly describe what documentation you are looking to add or improve. - -**Requirements** - -- [ ] Requirements go here diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/feature_request.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index ddd2fcca97..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature Request -about: A new feature to be added to the project -title: "feat: " -labels: feature ---- - -**Description** - -Clearly describe what you are looking to add. The more context the better. - -**Requirements** - -- [ ] Checklist of requirements to be fulfilled - -**Additional Context** - -Add any other context or screenshots about the feature request go here. diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/performance.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/performance.md deleted file mode 100644 index 699b8d45f2..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/performance.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Performance Update -about: A code change that improves performance -title: "perf: " -labels: performance ---- - -**Description** - -Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/refactor.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/refactor.md deleted file mode 100644 index 1626c57047..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/refactor.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Refactor -about: A code change that neither fixes a bug nor adds a feature -title: "refactor: " -labels: refactor ---- - -**Description** - -Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/revert.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/revert.md deleted file mode 100644 index 9d121dc56f..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/revert.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Revert Commit -about: Reverts a previous commit -title: "revert: " -labels: revert ---- - -**Description** - -Provide a link to a PR/Commit that you are looking to revert and why. - -**Requirements** - -- [ ] Change has been reverted -- [ ] No change in test coverage has happened -- [ ] A new ticket is created for any follow on work that needs to happen diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/style.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/style.md deleted file mode 100644 index 02244a7bdd..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/style.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Style Changes -about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) -title: "style: " -labels: style ---- - -**Description** - -Clearly describe what you are looking to change and why. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/test.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/test.md deleted file mode 100644 index 431a7ea764..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/test.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Test -about: Adding missing tests or correcting existing tests -title: "test: " -labels: test ---- - -**Description** - -List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/PULL_REQUEST_TEMPLATE.md b/frontend/appflowy_flutter/packages/flowy_svg/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 116993637f..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ - - -## Status - -**READY/IN DEVELOPMENT/HOLD** - -## Description - - - -## Type of Change - - - -- [ ] ✨ New feature (non-breaking change which adds functionality) -- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) -- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) -- [ ] 🧹 Code refactor -- [ ] ✅ Build configuration change -- [ ] 📝 Documentation -- [ ] 🗑️ Chore diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/cspell.json b/frontend/appflowy_flutter/packages/flowy_svg/.github/cspell.json deleted file mode 100644 index 29ea2f8e4b..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/cspell.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": "0.2", - "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", - "dictionaries": ["vgv_allowed", "vgv_forbidden"], - "dictionaryDefinitions": [ - { - "name": "vgv_allowed", - "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", - "description": "Allowed VGV Spellings" - }, - { - "name": "vgv_forbidden", - "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt", - "description": "Forbidden VGV Spellings" - } - ], - "useGitignore": true, - "words": [ - "flowy_svg" - ] -} diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/dependabot.yaml b/frontend/appflowy_flutter/packages/flowy_svg/.github/dependabot.yaml deleted file mode 100644 index 63b035cdea..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/dependabot.yaml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -enable-beta-ecosystems: true -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "pub" - directory: "/" - schedule: - interval: "daily" diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.github/workflows/main.yaml b/frontend/appflowy_flutter/packages/flowy_svg/.github/workflows/main.yaml deleted file mode 100644 index cf84703df4..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.github/workflows/main.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: ci - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - pull_request: - branches: - - main - -jobs: - semantic_pull_request: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 - - spell-check: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 - with: - includes: "**/*.md" - modified_files_only: false - - build: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 - with: - flutter_channel: stable diff --git a/frontend/appflowy_flutter/packages/flowy_svg/.gitignore b/frontend/appflowy_flutter/packages/flowy_svg/.gitignore deleted file mode 100644 index 8e19df2d9d..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# VSCode related -.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ -pubspec.lock - -# Web related -lib/generated_plugin_registrant.dart - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Test related -coverage \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml b/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml deleted file mode 100644 index 543c78a7d4..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index e87bb3fa01..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart +++ /dev/null @@ -1,290 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:path/path.dart' as path; - -import 'options.dart'; - -const languageKeywords = [ - 'abstract', - 'else', - 'import', - 'show', - 'as', - 'enum', - 'static', - 'assert', - 'export', - 'interface', - 'super', - 'async', - 'extends', - 'is', - 'switch', - 'await', - 'extension', - 'late', - 'sync', - 'base', - 'external', - 'library', - 'this', - 'break', - 'factory', - 'mixin', - 'throw', - 'case', - 'false', - 'new', - 'true', - 'catch', - 'final', - 'variable', - 'null', - 'try', - 'class', - 'final', - 'class', - 'on', - 'typedef', - 'const', - 'finally', - 'operator', - 'var', - 'continue', - 'for', - 'part', - 'void', - 'covariant', - 'Function', - 'required', - 'when', - 'default', - 'get', - 'rethrow', - 'while', - 'deferred', - 'hide', - 'return', - 'with', - 'do', - 'if', - 'sealed', - 'yield', - 'dynamic', - 'implements', - 'set', -]; - -void main(List args) { - if (_isHelpCommand(args)) { - _printHelperDisplay(); - } else { - generateSvgData(_generateOption(args)); - } -} - -bool _isHelpCommand(List args) { - return args.length == 1 && (args[0] == '--help' || args[0] == '-h'); -} - -void _printHelperDisplay() { - final parser = _generateArgParser(null); - log(parser.usage); -} - -Options _generateOption(List args) { - final generateOptions = Options(); - _generateArgParser(generateOptions).parse(args); - return generateOptions; -} - -ArgParser _generateArgParser(Options? generateOptions) { - final parser = ArgParser() - ..addOption( - 'source-dir', - abbr: 'S', - defaultsTo: '/assets/flowy_icons', - callback: (String? x) => generateOptions!.sourceDir = x, - help: 'Folder containing localization files', - ) - ..addOption( - 'output-dir', - abbr: 'O', - defaultsTo: '/lib/generated', - callback: (String? x) => generateOptions!.outputDir = x, - help: 'Output folder stores for the generated file', - ) - ..addOption( - 'name', - abbr: 'N', - defaultsTo: 'flowy_svgs.g.dart', - callback: (String? x) => generateOptions!.outputFile = x, - help: 'The name of the output file that this tool will generate', - ); - - return parser; -} - -Directory source(Options options) => Directory( - [ - Directory.current.path, - Directory.fromUri( - Uri.file( - options.sourceDir!, - windows: Platform.isWindows, - ), - ).path, - ].join(), - ); - -File output(Options options) => File( - [ - Directory.current.path, - Directory.fromUri( - Uri.file(options.outputDir!, windows: Platform.isWindows), - ).path, - Platform.pathSeparator, - File.fromUri( - Uri.file( - options.outputFile!, - windows: Platform.isWindows, - ), - ).path, - ].join(), - ); - -/// generates the svg data -Future generateSvgData(Options options) async { - // the source directory that this is targeting - final src = source(options); - - // the output directory that this is targeting - final out = output(options); - - var files = await dirContents(src); - files = files.where((f) => f.path.contains('.svg')).toList(); - - await generate(files, out, options); -} - -/// List the contents of the directory -Future> dirContents(Directory dir) { - final files = []; - final completer = Completer>(); - - dir.list(recursive: true).listen( - files.add, - onDone: () => completer.complete(files), - ); - return completer.future; -} - -/// Generate the abstract class for the FlowySvg data. -Future generate( - List files, - File output, - Options options, -) async { - final generated = File(output.path); - - // create the output file if it doesn't exist - if (!generated.existsSync()) { - generated.createSync(recursive: true); - } - - // content of the generated file - final builder = StringBuffer()..writeln(prelude); - files.whereType().forEach( - (element) => builder.writeln(lineFor(element, options)), - ); - builder.writeln(postlude); - - generated.writeAsStringSync(builder.toString()); -} - -String lineFor(File file, Options options) { - final name = varNameFor(file, options); - return " static const $name = FlowySvgData('${pathFor(file)}');"; -} - -String pathFor(File file) { - final relative = path.relative(file.path, from: Directory.current.path); - final uri = Uri.file(relative); - return uri.toFilePath(windows: false); -} - -String varNameFor(File file, Options options) { - final from = source(options).path; - - final relative = Uri.file(path.relative(file.path, from: from)); - - final parts = relative.pathSegments; - - final cleaned = parts.map(clean).toList(); - - var simplified = cleaned.reversed - // join all cleaned path segments with an underscore - .join('_') - // there are some cases where the segment contains a dart reserved keyword - // in this case, the path will be suffixed with an underscore which means - // there will be a double underscore, so we have to replace the double - // underscore with one underscore - .replaceAll(RegExp('_+'), '_'); - - // rename icon based on relative path folder name (16x, 24x, etc.) - for (final key in sizeMap.keys) { - simplified = simplified.replaceAll(key, sizeMap[key]!); - } - - return simplified; -} - -const sizeMap = { - r'$16x': 's', - r'$20x': 'm', - 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) { - final cleaned = segment - // replace all dashes with underscores (dash is invalid in - // a variable name) - .replaceAll('-', '_') - // replace all spaces with an underscore - .replaceAll(RegExp(r'\s+'), '_') - // replace all file extensions with an empty string - .replaceAll(RegExp(r'\.[^.]*$'), '') - // convert everything to lower case - .toLowerCase(); - - if (languageKeywords.contains(cleaned)) { - return '${cleaned}_'; - } else if (cleaned.startsWith(RegExp('[0-9]'))) { - return '\$$cleaned'; - } - return cleaned; -} - -/// The prelude for the generated file -const prelude = ''' -// DO NOT EDIT. This code is generated by the flowy_svg script - -// import the widget with from this package -import 'package:flowy_svg/flowy_svg.dart'; - -// export as convenience to the programmer -export 'package:flowy_svg/flowy_svg.dart'; - -/// A class to easily list all the svgs in the app -class FlowySvgs {'''; - -/// The postlude for the generated file -const postlude = ''' -} -'''; diff --git a/frontend/appflowy_flutter/packages/flowy_svg/bin/options.dart b/frontend/appflowy_flutter/packages/flowy_svg/bin/options.dart deleted file mode 100644 index d3476975d4..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/bin/options.dart +++ /dev/null @@ -1,21 +0,0 @@ -/// The options for the command line tool -class Options { - /// The source directory which the tool will use to generate the output file - String? sourceDir; - - /// The output directory which the tool will use to output the file(s) - String? outputDir; - - /// The name of the file that will be generated - String? outputFile; - - @override - String toString() { - return ''' -Options: - sourceDir: $sourceDir - outputDir: $outputDir - name: $outputFile -'''; - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_svg/coverage_badge.svg b/frontend/appflowy_flutter/packages/flowy_svg/coverage_badge.svg deleted file mode 100644 index 499e98ce2f..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/coverage_badge.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - coverage - coverage - 100% - 100% - - diff --git a/frontend/appflowy_flutter/packages/flowy_svg/lib/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/lib/flowy_svg.dart deleted file mode 100644 index 8e32c3e97c..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/lib/flowy_svg.dart +++ /dev/null @@ -1,4 +0,0 @@ -/// A Flutter package to generate Dart code for SVG files. -library flowy_svg; - -export 'src/flowy_svg.dart'; 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 deleted file mode 100644 index 1f861156eb..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart +++ /dev/null @@ -1,146 +0,0 @@ -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 - const FlowySvgData( - this.path, - ); - - /// The path to the svg data in appflowy assets/images - final String path; -} - -/// For icon that needs to change color when it is on hovered -/// -/// Get the hover color from ThemeData -class FlowySvg extends StatelessWidget { - /// Construct a FlowySvg Widget - const FlowySvg( - this.svg, { - super.key, - this.size, - this.color, - this.blendMode = BlendMode.srcIn, - this.opacity, - this.svgString, - }); - - /// 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; - - /// 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 - /// blend mode is null, but the blend mode defaults to [BlendMode.srcIn] - /// if it is not explicitly set to null. - final Color? color; - - /// The blend mode applied to the svg. - /// - /// If the blend mode is null then the icon color will not be applied. - /// Set both the icon color and blendMode in order to apply color to the - /// svg widget. - final BlendMode? blendMode; - - /// The opacity of the svg - /// - /// if null then use the opacity of the iconColor - 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 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, - ), - ); - } - - /// If the SVG's path does not start with `assets/`, it is - /// normalized and directed to `assets/images/` - /// - /// If the SVG does not end with `.svg`, then we append the file extension - /// - String _normalized() { - var path = svg.path; - - if (!path.toLowerCase().startsWith('assets/')) { - path = 'assets/images/$path'; - } - - if (!path.toLowerCase().endsWith('.svg')) { - path = '$path.svg'; - } - - return path; - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_svg/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_svg/pubspec.yaml deleted file mode 100644 index 5f012d5014..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_svg/pubspec.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: flowy_svg -description: AppFlowy Svgs -version: 0.1.0+1 -publish_to: none - -environment: - sdk: ">=3.0.0 <4.0.0" - flutter: 3.10.0 - -dependencies: - args: ^2.4.2 - flutter: - sdk: flutter - flutter_svg: ^2.0.7 - path: ^1.8.3 - -dev_dependencies: - very_good_analysis: ^5.0.0 diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1a393e1180..6e4e83c07f 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -5,79 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "61.0.0" analyzer: - dependency: "direct main" + dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "5.13.0" animations: dependency: transitive description: name: animations - sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb + sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 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" + version: "2.0.7" app_links: - dependency: "direct main" + dependency: transitive description: name: app_links - sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + sha256: "16725e716afd0634a5441654b1dda2b6c5557aa230884b5e1f41a5aa546a4cb6" 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.4.3" appflowy_backend: dependency: "direct main" description: @@ -89,29 +44,20 @@ packages: dependency: "direct main" description: path: "." - ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 - resolved-ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 + ref: a183c57 + resolved-ref: a183c57013071cb3192fcf3c9b1eeb89462179b7 url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git - version: "0.1.2" + version: "0.0.9" appflowy_editor: dependency: "direct main" description: path: "." - ref: "680222f" - resolved-ref: "680222f503f90d07c08c99c42764f9b08fd0f46c" + ref: "4f83b6f" + resolved-ref: "4f83b6feb92963f104f3f1f77a473a06aa4870f5" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "5.1.0" - appflowy_editor_plugins: - dependency: "direct main" - description: - path: "packages/appflowy_editor_plugins" - ref: "4efcff7" - resolved-ref: "4efcff720ed01dd4d0f5f88a9f1ff6f79f423caa" - url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git" - source: git - version: "0.0.6" + version: "1.0.4" appflowy_popover: dependency: "direct main" description: @@ -119,36 +65,22 @@ packages: relative: true source: path version: "0.0.1" - appflowy_result: - dependency: "direct main" - description: - path: "packages/appflowy_result" - 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: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "3.3.7" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.4.1" async: dependency: transitive description: @@ -157,129 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" - auto_size_text_field: - dependency: "direct main" - description: - name: auto_size_text_field - sha256: "41c90b2270e38edc6ce5c02e5a17737a863e65e246bdfc94565a38f3ec399144" - 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" - avatar_stack: - dependency: "direct main" - description: - name: avatar_stack - sha256: "354527ba139956fd6439e2c49199d8298d72afdaa6c4cd6f37f26b97faf21f7e" - 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" - bitsdojo_window: - dependency: "direct main" - description: - name: bitsdojo_window - sha256: "88ef7765dafe52d97d7a3684960fb5d003e3151e662c18645c1641c22b873195" - url: "https://pub.dev" - source: hosted - version: "0.1.6" - bitsdojo_window_linux: - dependency: transitive - description: - name: bitsdojo_window_linux - sha256: "9519c0614f98be733e0b1b7cb15b827007886f6fe36a4fb62cf3d35b9dd578ab" - url: "https://pub.dev" - source: hosted - version: "0.1.4" - bitsdojo_window_macos: - dependency: transitive - description: - name: bitsdojo_window_macos - sha256: f7c5be82e74568c68c5b8449e2c5d8fd12ec195ecd70745a7b9c0f802bb0268f - url: "https://pub.dev" - source: hosted - version: "0.1.4" - bitsdojo_window_platform_interface: - dependency: transitive - description: - name: bitsdojo_window_platform_interface - sha256: "65daa015a0c6dba749bdd35a0f092e7a8ba8b0766aa0480eb3ef808086f6e27c" - url: "https://pub.dev" - source: hosted - version: "0.1.2" - bitsdojo_window_windows: - dependency: transitive - description: - name: bitsdojo_window_windows - sha256: fa982cf61ede53f483e50b257344a1c250af231a3cdc93a7064dd6dc0d720b68 - url: "https://pub.dev" - source: hosted - version: "0.1.6" bloc: dependency: "direct main" description: name: bloc - sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" + sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "8.1.2" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: "1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b" + sha256: "5f41a3e391c89ccdade81a96233e1e5e5d01564e29e5fe180741fb23579399b9" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "9.1.2" boolean_selector: dependency: transitive description: @@ -292,50 +117,50 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.0" 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: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.0" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "220ae4553e50d7c21a17c051afc7b183d28a24a420502e842f303f8e4e6edced" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.4.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "88a57f2ac99849362e73878334caa9f06ee25f31d2adced882b8337838c84e1e" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "7.2.9" built_collection: dependency: transitive description: @@ -348,43 +173,18 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: "7dd62d9faf105c434f3d829bbe9c4be02ec67f5ed94832222116122df67c5452" url: "https://pub.dev" source: hosted - version: "8.9.3" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" + version: "8.6.0" calendar_view: dependency: "direct main" description: - path: "." - ref: "6fe0c98" - resolved-ref: "6fe0c989289b077569858d5472f3f7ec05b7746f" - url: "https://github.com/Xazin/flutter_calendar_view" - source: git - version: "1.0.5" + name: calendar_view + sha256: "58a8b851ac0a2d62770fd06ad30f06683bd40848a5dd1a1eca332f5a6064bd82" + url: "https://pub.dev" + source: hosted + version: "1.0.3" characters: dependency: transitive description: @@ -394,13 +194,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 +209,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,28 +229,28 @@ packages: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.4.0" collection: dependency: "direct main" description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.17.1" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + sha256: "8599ae9edca5ff96163fca3e36f8e481ea917d1e71cdad912c084b5579913f34" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "4.0.1" connectivity_plus_platform_interface: - dependency: transitive + dependency: "direct main" description: name: connectivity_plus_platform_interface sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a @@ -453,98 +261,74 @@ 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: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" url: "https://pub.dev" source: hosted - version: "1.11.1" - cross_cache: - 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" - url: "https://pub.dev" - source: hosted - version: "0.3.4+2" + version: "1.6.3" 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: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.1" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" dbus: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" 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" + version: "0.7.8" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da" + sha256: "499c61743e13909c13374a8c209075385858c614b9c0f2487b5f9995eeaf7369" url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "9.0.1" 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: @@ -553,54 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" - diffutil_dart: - dependency: "direct main" - 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: - name: dotted_border - sha256: "108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - easy_debounce: - dependency: transitive - description: - name: easy_debounce - sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236 - url: "https://pub.dev" - source: hosted - version: "2.0.3" easy_localization: dependency: "direct main" description: name: easy_localization - sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.2" easy_logger: dependency: transitive description: @@ -613,34 +357,26 @@ packages: dependency: "direct main" description: name: envied - sha256: "08a9012e5d93e1a816919a52e37c7b8367e73ebb8d52d1ca7dd6fcd875a2cd2c" + sha256: "60d3f5606c7b35bc6ef493e650d916b34351d8af2e58b7ac45881ba59dfcf039" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "0.3.0+3" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: "9a49ca9f3744069661c4f2c06993647699fae2773bca10b593fbb3228d081027" + sha256: dfdbe5dc52863e54c036a4c4042afbdf1bd528cb4c1e638ecba26228ba72e9e5 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "0.3.0+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 +385,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,82 +397,34 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.0.2" file: - dependency: "direct main" + dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.1.4" file_picker: - dependency: "direct overridden" + dependency: "direct main" description: name: file_picker - sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c" + sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" url: "https://pub.dev" source: hosted - version: "8.1.4" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.dev" - source: hosted - version: "0.9.3+2" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" - url: "https://pub.dev" - source: hosted - version: "0.9.4+2" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.dev" - source: hosted - version: "2.6.2" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" - url: "https://pub.dev" - source: hosted - version: "0.9.3+3" + version: "5.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,187 +446,82 @@ packages: relative: true source: path version: "0.0.1" - flowy_svg: - dependency: "direct main" + flowy_infra_ui_web: + dependency: transitive description: - path: "packages/flowy_svg" + path: "packages/flowy_infra_ui/flowy_infra_ui_web" relative: true source: path - version: "0.1.0+1" + version: "0.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_animate: - dependency: "direct main" - description: - name: flutter_animate - sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" - url: "https://pub.dev" - source: hosted - version: "4.5.2" flutter_bloc: dependency: "direct main" description: name: flutter_bloc - sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae url: "https://pub.dev" source: hosted - version: "9.1.0" - flutter_cache_manager: + version: "8.1.3" + flutter_colorpicker: dependency: "direct main" description: - path: flutter_cache_manager - ref: HEAD - resolved-ref: fbab857b1b1d209240a146d32f496379b9f62276 - 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" + name: flutter_colorpicker + sha256: "458a6ed8ea480eb16ff892aedb4b7092b2804affd7e046591fb03127e8d8ef8b" url: "https://pub.dev" source: hosted - version: "0.0.2" - flutter_chat_types: - dependency: transitive - description: - name: flutter_chat_types - sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9 - url: "https://pub.dev" - source: hosted - version: "3.6.2" - flutter_chat_ui: - dependency: "direct main" - description: - name: flutter_chat_ui - sha256: "2afd22eaebaf0f6ec8425048921479c3dd1a229604015dca05b174c6e8e44292" - url: "https://pub.dev" - source: hosted - version: "2.0.0-dev.1" + version: "1.0.3" flutter_driver: dependency: transitive description: flutter source: sdk version: "0.0.0" - flutter_emoji_mart: - dependency: "direct main" - description: - path: "." - ref: "355aa56" - resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8" - url: "https://github.com/LucasXu0/emoji_mart.git" - source: git - version: "1.0.2" - flutter_highlight: - dependency: transitive - description: - name: flutter_highlight - sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - flutter_link_previewer: - dependency: transitive - description: - name: flutter_link_previewer - sha256: "007069e60f42419fb59872beb7a3cc3ea21e9f1bdff5d40239f376fa62ca9f20" - url: "https://pub.dev" - source: hosted - version: "3.2.2" - flutter_linkify: - dependency: transitive - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "2.0.1" flutter_localizations: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" flutter_math_fork: dependency: "direct main" description: - name: flutter_math_fork - sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac" - url: "https://pub.dev" - source: hosted - version: "0.7.3" + path: "." + ref: de24059 + resolved-ref: de24059e28c6abb453be42c568f33aae1a4decba + url: "https://github.com/xazin/flutter_math_fork.git" + source: git + version: "0.6.3+1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" url: "https://pub.dev" source: hosted - version: "2.0.24" - flutter_shaders: - dependency: transitive - description: - name: flutter_shaders - sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" - url: "https://pub.dev" - source: hosted - version: "0.1.3" - flutter_slidable: - dependency: "direct main" - description: - name: flutter_slidable - sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 - 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" - flutter_sticky_header: - dependency: "direct overridden" - description: - name: flutter_sticky_header - sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" - url: "https://pub.dev" - source: hosted - version: "0.7.0" + version: "2.0.15" flutter_svg: - dependency: transitive + dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: "6ff8c902c8056af9736de2689f63f81c42e2d642b9f4c79dbf8790ae48b63012" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.6" 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 +531,55 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1" + sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c" url: "https://pub.dev" source: hosted - version: "8.2.10" + version: "8.2.2" freezed: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "2edb9ef971d0f803860ecd9084afd48c717d002141ad77b69be3e976bee7190e" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.3.4" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.2.0" 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: "578537de508c62c2875a6fdaa5dc71033283551ac7a32b8b8ef405c6c5823273" + url: "https://pub.dev" + source: hosted + version: "1.3.0" get_it: dependency: "direct main" description: name: get_it - sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 + sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "7.6.0" glob: dependency: transitive description: @@ -1013,38 +588,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" - url: "https://pub.dev" - source: hosted - version: "14.6.3" google_fonts: dependency: "direct main" description: name: google_fonts - sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + sha256: "2776c66b3e97c6cdd58d1bd3281548b074b64f1fd5c8f82391f7456e38849567" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "4.0.5" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: "3306606658484a05fc885aea15f9fa65bcc28194f35ef294de3a34d01393b928" + url: "https://pub.dev" + source: hosted + version: "1.8.0" graphs: dependency: transitive description: name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.3.2" - gtk: - dependency: transitive - description: - name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" - source: hosted - version: "2.1.0" + version: "2.3.1" highlight: dependency: "direct main" description: @@ -1062,7 +629,7 @@ packages: source: hosted version: "2.2.3" hive_flutter: - dependency: "direct main" + dependency: transitive description: name: hive_flutter sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc @@ -1073,130 +640,42 @@ packages: dependency: "direct main" description: name: hotkey_manager - sha256: "8aaa0aeaca7015b8c561a58d02eb7ebba95e93357fc9540398c5751ee24afd7c" + sha256: "39b352b6dabb806fd53122d6c95ded1174df882d2db01eedc31cc046f8cc7b6c" url: "https://pub.dev" source: hosted - version: "0.1.8" + version: "0.1.7" html: 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: "4c3f04bfb64d3efd508d06b41b825542f08122d30bda4933fb95c069d22a4fa3" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.0.0" 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 - description: - name: iconsax_flutter - sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d" - 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" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c - url: "https://pub.dev" - source: hosted - version: "0.8.12+20" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" - url: "https://pub.dev" - source: hosted - version: "0.8.12+2" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" - url: "https://pub.dev" - source: hosted - version: "2.10.1" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" + version: "4.0.2" integration_test: dependency: "direct dev" description: flutter @@ -1206,34 +685,26 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.18.0" + intl_utils: + dependency: transitive + description: + name: intl_utils + sha256: a509a2ada4d12c4dc70f9ca35c2fddf75f8b402409ac1a9e1b3dd8065681986b + url: "https://pub.dev" + source: hosted + version: "2.8.3" io: dependency: transitive description: name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.5" - irondash_engine_context: - dependency: transitive - description: - name: irondash_engine_context - sha256: cd7b769db11a2b5243b037c8a9b1ecaef02e1ae27a2d909ffa78c1dad747bb10 - url: "https://pub.dev" - source: hosted - version: "0.5.4" - irondash_message_channel: - dependency: transitive - description: - name: irondash_message_channel - sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 - url: "https://pub.dev" - source: hosted - version: "0.7.0" + version: "1.0.4" isolates: dependency: transitive description: @@ -1254,50 +725,26 @@ 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: "61a60716544392a82726dd0fa1dd6f5f1fd32aec66422b6e229e7b90d52325c4" url: "https://pub.dev" source: hosted - version: "6.9.0" - keyboard_height_plugin: - dependency: "direct main" - description: - name: keyboard_height_plugin - sha256: "3a51c8ebb43465ebe0b3bad17f3b6d945421e58011f3f5a08134afe69a3d775f" - url: "https://pub.dev" - source: hosted - version: "0.1.5" - leak_tracker: - dependency: "direct main" - description: - name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" - url: "https://pub.dev" - source: hosted - version: "10.0.7" - leak_tracker_flutter_testing: + version: "6.7.0" + jwt_decode: dependency: transitive description: - name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb url: "https://pub.dev" source: hosted - version: "3.0.8" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" + version: "0.3.1" linked_scroll_controller: dependency: "direct main" description: @@ -1306,14 +753,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - linkify: - dependency: transitive - description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" - url: "https://pub.dev" - source: hosted - version: "5.0.0" lint: dependency: transitive description: @@ -1326,106 +765,82 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "2.1.0" loading_indicator: dependency: transitive description: name: loading_indicator - sha256: a101ffb2aa3e646137d7810bfa90b50525dd3f72c01235b6df7491cf6af6f284 + sha256: "5cb15810fc3d8872230d086b4810d9f9bbcd6e7d208a38f7cd6bc81fb2650e4b" url: "https://pub.dev" source: hosted - version: "3.1.1" - local_notifier: - dependency: "direct main" + version: "3.1.0" + logger: + dependency: transitive description: - name: local_notifier - sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025 + name: logger + sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7 url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.3.0" 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: "8e332924094383133cee218b676871f42db2514f1f6ac617b6cf6152a7faab8e" url: "https://pub.dev" source: hosted - version: "7.3.0" - markdown_widget: - dependency: "direct main" - description: - name: markdown_widget - sha256: "216dced98962d7699a265344624bc280489d739654585ee881c95563a3252fac" - url: "https://pub.dev" - source: hosted - version: "2.3.2+6" + version: "7.1.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.2.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.9.1" mime: - dependency: "direct main" - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mockito: dependency: transitive description: - name: mockito - sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 - url: "https://pub.dev" - source: hosted - version: "5.4.5" - mocktail: - dependency: "direct dev" - description: - name: mocktail - sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted version: "1.0.4" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53" + url: "https://pub.dev" + source: hosted + version: "0.3.0" nanoid: dependency: "direct main" description: @@ -1458,142 +873,94 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - numerus: - dependency: "direct main" - description: - name: numerus - sha256: a17a3f34527497e89378696a76f382b40dc534c4a57b3778de246ebc1ce2ca99 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - 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" 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: "28386bbe89ab5a7919a47cea99cdd1128e5a6e0bbd7eaafe20440ead84a15de3" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "4.0.1" 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: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" - path_drawing: - dependency: transitive - description: - name: path_drawing - sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "1.8.3" path_parsing: 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: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.0.15" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.0.27" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.2.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.1.11" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 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.1.6" percent_indicator: dependency: "direct main" description: @@ -1602,86 +969,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.3" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" - url: "https://pub.dev" - source: hosted - version: "11.3.1" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" - url: "https://pub.dev" - source: hosted - version: "12.0.13" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 - url: "https://pub.dev" - source: hosted - version: "9.4.5" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" - source: hosted - version: "0.1.3+5" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 - url: "https://pub.dev" - source: hosted - version: "4.2.3" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "6.0.2" - pixel_snap: - dependency: transitive - description: - name: pixel_snap - sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" - url: "https://pub.dev" - source: hosted - version: "0.1.5" + version: "5.4.0" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.0" plugin_platform_interface: - dependency: "direct dev" + dependency: transitive description: name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" pool: dependency: transitive description: @@ -1690,71 +1009,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "42abd4bf3322af3eb0d286ca2fca7cc28baae52b805761dfa7ab0d206ee072a3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" process: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "4.2.4" protobuf: dependency: "direct main" description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "467c7ccf3fee0272d5cb84bf20debc509f9a4f69ce06e5f7a8b749d7e6962085" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "2.0.0" provider: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.0.5" pub_semver: 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: "13f6a62244bca7562b47658e3f92e5eeeb79a46d58ad4a97ad536e4ba5e97086" url: "https://pub.dev" source: hosted - version: "3.0.2" - recase: - dependency: transitive - description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 - url: "https://pub.dev" - source: hosted - version: "4.1.0" - reorderable_tabbar: - dependency: "direct main" - description: - path: "." - ref: "93c4977" - resolved-ref: "93c4977ffab68906694cdeaea262be6045543c94" - url: "https://github.com/LucasXu0/reorderable_tabbar" - source: git - version: "1.0.6" + version: "1.1.0" reorderables: dependency: "direct main" description: @@ -1763,14 +1073,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" - run_with_network_images: - dependency: "direct dev" + retry: + dependency: transitive description: - name: run_with_network_images - sha256: "8bf2de4e5120ab24037eda09596408938aa8f5b09f6afabd49683bd01c7baa36" + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" url: "https://pub.dev" source: hosted - version: "0.0.1" + version: "3.1.2" + rich_clipboard: + dependency: transitive + description: + name: rich_clipboard + sha256: "48bfc84a0d3eeec5692b3afd0277aa658a7c95d1dbda72bb623188fba6a8e253" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + rich_clipboard_android: + dependency: transitive + description: + name: rich_clipboard_android + sha256: "72725b248d5359a7ad6db2fea5aef921015ba9a00af275cbce3721a4fef20356" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + rich_clipboard_ios: + dependency: transitive + description: + name: rich_clipboard_ios + sha256: "9d6bc037463b1b24cb14ae35ee9d7530bd6b2bdb15b30909fb47a1af01bf3233" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + rich_clipboard_linux: + dependency: transitive + description: + name: rich_clipboard_linux + sha256: "0d0ab273afd60cb7314d01fdf3994fa01be2be79528f448241d9d70ea19b3db9" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + rich_clipboard_macos: + dependency: transitive + description: + name: rich_clipboard_macos + sha256: "1aeb409e267576baaced347549e42dabc59895b10b2e09dabd9f753f469deb3e" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + rich_clipboard_platform_interface: + dependency: transitive + description: + name: rich_clipboard_platform_interface + sha256: a1cbf255719cd4e340d33eca02b619d9ffb9cb571f1905e80b9345d4266e893d + url: "https://pub.dev" + source: hosted + version: "1.0.0" + rich_clipboard_web: + dependency: transitive + description: + name: rich_clipboard_web + sha256: c1dd2b75b8ce83fed0027828900bbfd5c33c0f8ff22efb266931db5aa7acffa0 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + rich_clipboard_windows: + dependency: transitive + description: + name: rich_clipboard_windows + sha256: fa2a28e75ce4bcc9efc6d5d0e9788b76716cdaf3b7063c141fe8af12a315f414 + url: "https://pub.dev" + source: hosted + version: "1.0.2" rxdart: dependency: transitive description: @@ -1779,183 +1153,78 @@ 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: - name: scaled_app - sha256: a2ad9f22cf2200a5ce455b59c5ea7bfb09a84acfc52452d1db54f4958c99d76a - url: "https://pub.dev" - source: hosted - version: "2.3.0" screen_retriever: dependency: transitive description: name: screen_retriever - sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323" 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" - scroll_to_index: - dependency: "direct main" - description: - name: scroll_to_index - sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - scrollable_positioned_list: - dependency: "direct main" - description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" - 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 - url: "https://pub.dev" - source: hosted - version: "10.1.4" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b - url: "https://pub.dev" - source: hosted - version: "5.0.2" + version: "0.1.6" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a + sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.1.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: bf808be89fe9dc467475e982c1db6c2faf3d2acf54d526cd5ec37d86c99dbd84 + sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.1.4" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.2.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.2.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.2.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" url: "https://pub.dev" source: hosted - version: "2.4.1" - sheet: - dependency: "direct main" - description: - path: sheet - ref: e44458d - resolved-ref: e44458d2359565324e117bb3d41da04f5e60362e - url: "https://github.com/jamesblasco/modal_bottom_sheet" - source: git - version: "1.0.0" + version: "2.2.0" shelf: 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,26 +1237,50 @@ 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" + sign_in_with_apple: + dependency: transitive + description: + name: sign_in_with_apple + sha256: ac3b113767dfdd765078c507dad9d4d9fe96b669cc7bd88fc36fc15376fb3400 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + sign_in_with_apple_platform_interface: + dependency: transitive + description: + name: sign_in_with_apple_platform_interface + sha256: a5883edee09ed6be19de19e7d9f618a617fe41a6fa03f76d082dfb787e9ea18d + url: "https://pub.dev" + source: hosted + version: "1.0.0" + sign_in_with_apple_web: + dependency: transitive + description: + name: sign_in_with_apple_web + sha256: "44b66528f576e77847c14999d5e881e17e7223b7b0625a185417829e5306f47a" + url: "https://pub.dev" + source: hosted + version: "1.0.1" simple_gesture_detector: dependency: transitive description: name: simple_gesture_detector - sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + sha256: "86d08f85f1f58583b7b4b941d989f48ea6ce08c1724a1d10954a277c2ec36592" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.0" sized_context: dependency: "direct main" description: @@ -2000,143 +1293,87 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.0" - sliver_tools: - dependency: transitive - description: - name: sliver_tools - sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 - url: "https://pub.dev" - source: hosted - version: "0.2.12" + version: "0.0.99" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.3.2" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.3" 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: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 url: "https://pub.dev" source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" - 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" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" - 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: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.11.0" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: e14434a4cc16b01f2e96f3c646e43fb0bb16624b279a65a34da889cffe4b083c + url: "https://pub.dev" + source: hosted + version: "1.4.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: 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" - string_validator: - dependency: "direct main" - description: - name: string_validator - sha256: a278d038104aa2df15d0e09c47cb39a49f907260732067d0034dc2f2e4e2ac94 - url: "https://pub.dev" - source: hosted - version: "1.1.0" + version: "1.2.0" styled_widget: dependency: "direct main" description: @@ -2145,22 +1382,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" - super_clipboard: - dependency: "direct main" - description: - name: super_clipboard - sha256: "4a6ae6dfaa282ec1f2bff750976f535517ed8ca842d5deae13985eb11c00ac1f" - url: "https://pub.dev" - source: hosted - version: "0.8.24" - super_native_extensions: + supabase: dependency: transitive description: - name: super_native_extensions - sha256: a433bba8186cd6b707560c42535bf284804665231c00bca86faf1aa4968b7637 + name: supabase + sha256: "8f89e406d1c0101409a9c5d5560ed391d6d3636d2e077336905f3eee18622073" url: "https://pub.dev" source: hosted - version: "0.8.24" + version: "1.9.0" + supabase_flutter: + dependency: "direct main" + description: + name: supabase_flutter + sha256: "809c67c296d4a0690fdc8e5f952a5e18b3ebd145867f1cb3f8f80248b22a56ae" + url: "https://pub.dev" + source: hosted + version: "1.10.0" sync_http: dependency: transitive description: @@ -2169,54 +1406,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" - synchronized: - dependency: "direct main" - description: - name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" - url: "https://pub.dev" - source: hosted - version: "3.3.0+3" - tab_indicator_styler: - dependency: transitive - description: - name: tab_indicator_styler - sha256: "9e7e90367e20f71f3882fc6578fdcced35ab1c66ab20fcb623cdcc20d2796c76" - url: "https://pub.dev" - source: hosted - version: "2.0.0" table_calendar: dependency: "direct main" description: name: table_calendar - sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 + sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478" 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.0.9" term_glyph: dependency: transitive description: @@ -2229,50 +1426,50 @@ packages: dependency: transitive description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.24.1" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.5.1" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.5.1" + textfield_tags: + dependency: "direct main" + description: + name: textfield_tags + sha256: c1d215f481e7e8da5c79719825e595db4f829bf1ad3fce4c7ce43d340aa72683 + url: "https://pub.dev" + source: hosted + version: "2.0.2" time: dependency: "direct main" description: name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.3" 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,156 +1482,122 @@ packages: dependency: transitive description: name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.4.0" - universal_html: - dependency: transitive - description: - name: universal_html - sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" - url: "https://pub.dev" - source: hosted - version: "2.2.4" + version: "1.3.2" universal_io: dependency: transitive description: name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.0" 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" - unsplash_client: - dependency: "direct main" - description: - path: "." - ref: a8411fc - resolved-ref: a8411fcead178834d1f4572f64dc78b059ffa221 - url: "https://github.com/LucasXu0/unsplash_client.git" - source: git - version: "2.2.0" + version: "1.0.0+1" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "1a5848f598acc5b7d8f7c18b8cb834ab667e59a13edc3c93e9d09cf38cc6bc87" url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.0.34" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.0.5" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.0.5" url_launcher_platform_interface: - dependency: "direct dev" + dependency: transitive description: name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.0.17" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" url: "https://pub.dev" source: hosted - version: "3.1.4" - url_protocol: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "77a84201ed8ca50082f4248f3a373d053b1c0462" - url: "https://github.com/LucasXu0/flutter_url_protocol.git" - source: git - version: "1.0.0" + version: "3.0.6" uuid: - dependency: "direct overridden" - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" - value_layout_builder: dependency: transitive description: - name: value_layout_builder - sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "3.0.7" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" + sha256: b96f10cbdfcbd03a65758633a43e7d04574438f059b1043104b5d61b23d38a4f url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.6" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + sha256: "57a8e6e24662a3bdfe3b3d61257db91768700c0b8f844e235877b56480f31c69" url: "https://pub.dev" source: hosted - version: "1.1.13" + version: "1.1.6" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "7430f5d834d0db4560d7b19863362cd892f1e52b43838553a3c5cdfc9ab28e5b" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.6" vector_math: dependency: transitive description: @@ -2443,14 +1606,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,146 +1618,130 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "11.3.0" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web: - dependency: transitive - description: - name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 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" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "2.4.0" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.0" webview_flutter: dependency: transitive description: name: webview_flutter - sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec" + sha256: "5604dac1178680a34fbe4a08c7b69ec42cca6601dc300009ec9ff69bef284cc2" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.2.1" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: d1ee28f44894cbabb1d94cc42f9980297f689ff844d067ec50ff88d86e27d63f + sha256: "57a22c86065375c1598b57224f92d6008141be0c877c64100de8bfb6f71083d8" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "3.7.1" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + sha256: "656e2aeaef318900fffd21468b6ddc7958c7092a642f0e7220bac328b70d4a81" 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" + version: "2.3.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "4adc14ea9a770cc9e2c8f1ac734536bd40e82615bd0fa6b94be10982de656cc7" + sha256: "6bbc6ade302b842999b27cbaa7171241c273deea8a9c73f92ceb3d811c767de2" url: "https://pub.dev" source: hosted - version: "3.17.0" + version: "3.4.4" win32: dependency: transitive description: name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "4.1.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.0" window_manager: dependency: "direct main" description: name: window_manager - sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" + sha256: "95096fede562cbb65f30d38b62d819a458f59ba9fe4a317f6cee669710f6676b" url: "https://pub.dev" source: hosted - version: "0.4.3" + version: "0.3.4" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.0.0" xml: - dependency: "direct main" + dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: 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: "7809f6517bafd0a7b3d0be63cd5f952ae5c030d682250e8aa9ed7002eaac5ff8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=3.6.2 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.1" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 1e92765ff6..ae73a6e4f2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -1,261 +1,147 @@ 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.2.3 environment: - flutter: ">=3.27.4" - sdk: ">=3.3.0 <4.0.0" + sdk: ">=3.0.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 - appflowy_backend: - path: packages/appflowy_backend - 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 - 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 - - # BitsDojo Window for Windows - bitsdojo_window: ^0.1.6 - bloc: ^9.0.0 - cached_network_image: ^3.3.0 - 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 - 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 - 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 - - # 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 - 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 - 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: - - # 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 - - analyzer: 6.11.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_test: + flutter_localizations: sdk: flutter - freezed: ^2.4.7 - integration_test: - sdk: flutter - json_serializable: ^6.7.1 - - mocktail: ^1.0.1 - - plugin_platform_interface: any - run_with_network_images: ^0.0.1 - url_launcher_platform_interface: any - -dependency_overrides: - http: ^1.0.0 - device_info_plus: ^11.2.2 - - url_protocol: + appflowy_backend: + path: packages/appflowy_backend + flowy_infra_ui: + path: packages/flowy_infra_ui + flowy_infra: + path: packages/flowy_infra + appflowy_board: + # path: packages/appflowy_board git: - url: https://github.com/LucasXu0/flutter_url_protocol.git - commit: 737681d - + url: https://github.com/AppFlowy-IO/appflowy-board.git + ref: a183c57 + # appflowy_editor: ^1.0.4 appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "680222f" + ref: 4f83b6f + appflowy_popover: + path: packages/appflowy_popover - appflowy_editor_plugins: + # third party packages + intl: ^0.18.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: git: - url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git - path: "packages/appflowy_editor_plugins" - ref: "4efcff7" + url: https://github.com/xazin/flutter_math_fork.git + ref: de24059 + dartz: ^0.10.1 + 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: ^4.0.1 + url_launcher: ^6.1.11 + clipboard: ^0.1.3 + connectivity_plus: ^4.0.1 + connectivity_plus_platform_interface: ^1.2.2 + easy_localization: ^3.0.2 + textfield_tags: ^2.0.2 + device_info_plus: ^9.0.1 + 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 + protobuf: "2.0.0" + charcode: ^1.3.1 + collection: ^1.17.1 + bloc: ^8.1.2 + shared_preferences: ^2.1.1 + google_fonts: ^4.0.5 + file_picker: ^5.3.1 + percent_indicator: ^4.2.3 + calendar_view: ^1.0.3 + window_manager: ^0.3.4 + http: ^1.0.0 + path: ^1.8.3 + mocktail: ^0.3.0 + archive: ^3.3.7 + flutter_svg: ^2.0.6 + nanoid: ^1.0.0 + supabase_flutter: ^1.10.0 + envied: ^0.3.0+3 - sheet: - git: - url: https://github.com/jamesblasco/modal_bottom_sheet - ref: e44458d - path: sheet +dev_dependencies: + flutter_lints: ^2.0.1 - uuid: ^4.4.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + build_runner: ^2.4.4 + freezed: ^2.3.4 + bloc_test: ^9.1.2 + json_serializable: ^6.7.0 + envied_generator: ^0.3.0+3 - flutter_cache_manager: - git: - url: https://github.com/LucasXu0/flutter_cache_manager.git - commit: fbab857b1b1d209240a146d32f496379b9f62276 - path: flutter_cache_manager +dependency_overrides: + http: ^1.0.0 - 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 @@ -276,36 +162,24 @@ flutter: weight: 800 - asset: assets/google_fonts/Poppins/Poppins-ExtraBold.ttf weight: 900 - - family: RobotoMono - fonts: - - 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: - assets/images/ - - assets/images/appearance/ - - 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/home/ + - assets/images/editor/align/ + - assets/images/editor/ + - assets/images/grid/ - assets/images/emoji/ + - assets/images/grid/field/ + - assets/images/common/ - assets/images/login/ + - assets/images/grid/setting/ - assets/translations/ - - assets/icons/icons.json - - assets/fonts/ # The following assets will be excluded in release. # BEGIN: EXCLUDE_IN_RELEASE - assets/test/workspaces/ - - assets/test/images/ - assets/template/ - assets/test/workspaces/markdowns/ - assets/test/workspaces/database/ 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/app_setting_test/appearance_test.dart b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart index d9cd57757e..c0f66d7e20 100644 --- a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart @@ -1,9 +1,7 @@ import 'package:appflowy/user/application/user_settings_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_backend/protobuf/flowy-user/user_setting.pb.dart'; +import 'package:appflowy/workspace/application/appearance.dart'; import 'package:bloc_test/bloc_test.dart'; -import 'package:flowy_infra/theme.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -18,36 +16,26 @@ void main() { group('$AppearanceSettingsCubit', () { late AppearanceSettingsPB appearanceSetting; - late DateTimeSettingsPB dateTimeSettings; - setUp(() async { appearanceSetting = await UserSettingsBackendService().getAppearanceSetting(); - dateTimeSettings = - await UserSettingsBackendService().getDateTimeSettings(); await blocResponseFuture(); }); blocTest( 'default theme', - build: () => AppearanceSettingsCubit( - appearanceSetting, - dateTimeSettings, - AppTheme.fallback, - ), + build: () => AppearanceSettingsCubit(appearanceSetting), verify: (bloc) { - expect(bloc.state.font, defaultFontFamily); + // expect(bloc.state.appTheme.info.name, "light"); + expect(bloc.state.font, 'Poppins'); + expect(bloc.state.monospaceFont, 'SF Mono'); expect(bloc.state.themeMode, ThemeMode.system); }, ); blocTest( 'save key/value', - build: () => AppearanceSettingsCubit( - appearanceSetting, - dateTimeSettings, - AppTheme.fallback, - ), + build: () => AppearanceSettingsCubit(appearanceSetting), act: (bloc) { bloc.setKeyValue("123", "456"); }, @@ -58,11 +46,7 @@ void main() { blocTest( 'remove key/value', - build: () => AppearanceSettingsCubit( - appearanceSetting, - dateTimeSettings, - AppTheme.fallback, - ), + build: () => AppearanceSettingsCubit(appearanceSetting), act: (bloc) { bloc.setKeyValue("123", null); }, @@ -70,17 +54,5 @@ void main() { expect(bloc.getValue("123"), null); }, ); - - blocTest( - 'initial state uses fallback theme', - build: () => AppearanceSettingsCubit( - appearanceSetting, - dateTimeSettings, - AppTheme.fallback, - ), - verify: (bloc) { - expect(bloc.state.appTheme.themeName, AppTheme.fallback.themeName); - }, - ); }); } diff --git a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart deleted file mode 100644 index 2f369cd0cf..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - group('DocumentAppearanceCubit', () { - late SharedPreferences preferences; - late DocumentAppearanceCubit cubit; - - setUpAll(() async { - SharedPreferences.setMockInitialValues({}); - }); - - setUp(() async { - preferences = await SharedPreferences.getInstance(); - cubit = DocumentAppearanceCubit(); - }); - - tearDown(() async { - await preferences.clear(); - await cubit.close(); - }); - - test('Initial state', () { - expect(cubit.state.fontSize, 16.0); - expect(cubit.state.fontFamily, defaultFontFamily); - }); - - test('Fetch document appearance from SharedPreferences', () async { - await preferences.setDouble(KVKeys.kDocumentAppearanceFontSize, 18.0); - await preferences.setString( - KVKeys.kDocumentAppearanceFontFamily, - 'Arial', - ); - - await cubit.fetch(); - - expect(cubit.state.fontSize, 18.0); - expect(cubit.state.fontFamily, 'Arial'); - }); - - test('Sync font size to SharedPreferences', () async { - await cubit.syncFontSize(20.0); - - final fontSize = - preferences.getDouble(KVKeys.kDocumentAppearanceFontSize); - expect(fontSize, 20.0); - expect(cubit.state.fontSize, 20.0); - }); - - test('Sync font family to SharedPreferences', () async { - await cubit.syncFontFamily('Helvetica'); - - final fontFamily = - preferences.getString(KVKeys.kDocumentAppearanceFontFamily); - expect(fontFamily, 'Helvetica'); - expect(cubit.state.fontFamily, 'Helvetica'); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart index 4f855d4fb3..556ad69420 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart @@ -1,6 +1,5 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; @@ -16,42 +15,26 @@ void main() { final context = await boardTest.createTestBoard(); final databaseController = DatabaseController(view: context.gridView); final boardBloc = BoardBloc( + view: context.gridView, databaseController: databaseController, )..add(const BoardEvent.initial()); await boardResponseFuture(); - List groupIds = boardBloc.state.maybeMap( - orElse: () => const [], - ready: (value) => value.groupIds, - ); - String lastGroupId = groupIds.last; + final groupId = boardBloc.state.groupIds.first; - // the group at index 3 is the 'No status' group; - assert(boardBloc.groupControllers[lastGroupId]!.group.rows.isEmpty); + // the group at index 0 is the 'No status' group; + assert(boardBloc.groupControllers[groupId]!.group.rows.isEmpty); assert( - groupIds.length == 4, - 'but receive ${groupIds.length}', + boardBloc.state.groupIds.length == 4, + 'but receive ${boardBloc.state.groupIds.length}', ); - boardBloc.add( - BoardEvent.createRow( - groupIds[3], - OrderObjectPositionTypePB.End, - null, - null, - ), - ); + boardBloc.add(BoardEvent.createBottomRow(boardBloc.state.groupIds[0])); await boardResponseFuture(); - groupIds = boardBloc.state.maybeMap( - orElse: () => [], - ready: (value) => value.groupIds, - ); - lastGroupId = groupIds.last; - assert( - boardBloc.groupControllers[lastGroupId]!.group.rows.length == 1, - 'but receive ${boardBloc.groupControllers[lastGroupId]!.group.rows.length}', + boardBloc.groupControllers[groupId]!.group.rows.length == 1, + 'but receive ${boardBloc.groupControllers[groupId]!.group.rows.length}', ); }); } 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..6b851f0207 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 @@ -1,6 +1,7 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; @@ -15,6 +16,7 @@ void main() { test('create build-in kanban board test', () async { final context = await boardTest.createTestBoard(); final boardBloc = BoardBloc( + view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); @@ -26,21 +28,25 @@ void main() { test('edit kanban board field name test', () async { final context = await boardTest.createTestBoard(); final boardBloc = BoardBloc( + view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); final fieldInfo = context.singleSelectFieldContext(); + final loader = FieldTypeOptionLoader( + viewId: context.gridView.id, + field: fieldInfo.field, + ); final editorBloc = FieldEditorBloc( - viewId: context.gridView.id, - fieldInfo: fieldInfo, - fieldController: context.fieldController, - isNew: false, - ); + isGroupField: fieldInfo.isGroupField, + loader: loader, + field: fieldInfo.field, + )..add(const FieldEditorEvent.initial()); await boardResponseFuture(); - editorBloc.add(const FieldEditorEvent.renameField('Hello world')); + editorBloc.add(const FieldEditorEvent.updateName('Hello world')); await boardResponseFuture(); // assert the groups were not changed @@ -58,6 +64,7 @@ void main() { test('create a new field in kanban board test', () async { final context = await boardTest.createTestBoard(); final boardBloc = BoardBloc( + view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart index ad4d96f5eb..dca6f39d14 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart @@ -1,6 +1,6 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/setting/group_bloc.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -17,6 +17,7 @@ void main() { test('group by checkbox field test', () async { final context = await boardTest.createTestBoard(); final boardBloc = BoardBloc( + view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); @@ -34,7 +35,7 @@ void main() { final checkboxField = context.fieldContexts.last.field; final gridGroupBloc = DatabaseGroupBloc( viewId: context.gridView.id, - databaseController: context.databaseController, + fieldController: context.fieldController, )..add(const DatabaseGroupEvent.initial()); gridGroupBloc.add( DatabaseGroupEvent.setGroupByField( 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 deleted file mode 100644 index 51bd537159..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart +++ /dev/null @@ -1,116 +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/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/setting/group_bloc.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'util.dart'; - -void main() { - late AppFlowyBoardTest boardTest; - - setUpAll(() async { - boardTest = await AppFlowyBoardTest.ensureInitialized(); - }); - - test('group by date field test', () async { - final context = await boardTest.createTestBoard(); - final boardBloc = BoardBloc( - databaseController: DatabaseController(view: context.gridView), - )..add(const BoardEvent.initial()); - await boardResponseFuture(); - - // assert the initial values - assert(boardBloc.groupControllers.values.length == 4); - assert(context.fieldContexts.length == 2); - - await context.createField(FieldType.DateTime); - await boardResponseFuture(); - assert(context.fieldContexts.length == 3); - - final dateField = context.fieldContexts.last.field; - final cellController = context.makeCellControllerFromFieldId(dateField.id) - as DateCellController; - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: getIt(), - ); - await boardResponseFuture(); - - bloc.add(DateCellEditorEvent.updateDateTime(DateTime.now())); - await boardResponseFuture(); - - final gridGroupBloc = DatabaseGroupBloc( - viewId: context.gridView.id, - databaseController: context.databaseController, - )..add(const DatabaseGroupEvent.initial()); - gridGroupBloc.add( - DatabaseGroupEvent.setGroupByField( - dateField.id, - dateField.fieldType, - ), - ); - await boardResponseFuture(); - - assert(boardBloc.groupControllers.values.length == 2); - assert( - boardBloc.boardController.groupDatas.last.headerData.groupName == - LocaleKeys.board_dateCondition_today.tr(), - ); - }); - - test('group by date field with condition', () async { - final context = await boardTest.createTestBoard(); - final boardBloc = BoardBloc( - databaseController: DatabaseController(view: context.gridView), - )..add(const BoardEvent.initial()); - await boardResponseFuture(); - - // assert the initial values - assert(boardBloc.groupControllers.values.length == 4); - assert(context.fieldContexts.length == 2); - - await context.createField(FieldType.DateTime); - await boardResponseFuture(); - assert(context.fieldContexts.length == 3); - - final dateField = context.fieldContexts.last.field; - final cellController = context.makeCellControllerFromFieldId(dateField.id) - as DateCellController; - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: getIt(), - ); - await boardResponseFuture(); - - bloc.add(DateCellEditorEvent.updateDateTime(DateTime.now())); - await boardResponseFuture(); - - final gridGroupBloc = DatabaseGroupBloc( - viewId: context.gridView.id, - databaseController: context.databaseController, - )..add(const DatabaseGroupEvent.initial()); - final settingContent = DateGroupConfigurationPB() - ..condition = DateConditionPB.Year; - gridGroupBloc.add( - DatabaseGroupEvent.setGroupByField( - dateField.id, - dateField.fieldType, - settingContent.writeToBuffer(), - ), - ); - await boardResponseFuture(); - - assert(boardBloc.groupControllers.values.length == 2); - assert( - boardBloc.boardController.groupDatas.last.headerData.groupName == - DateTime.now().year.toString(), - ); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart index e2a794b3c4..f8443ddf77 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart @@ -1,8 +1,8 @@ -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/setting/group_bloc.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -27,7 +27,7 @@ void main() { // set grouped by the new multi-select field" final gridGroupBloc = DatabaseGroupBloc( viewId: context.gridView.id, - databaseController: context.databaseController, + fieldController: context.fieldController, )..add(const DatabaseGroupEvent.initial()); await boardResponseFuture(); @@ -39,8 +39,9 @@ void main() { ); await boardResponseFuture(); - // assert only have the 'No status' group + //assert only have the 'No status' group final boardBloc = BoardBloc( + view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); @@ -48,6 +49,12 @@ void main() { boardBloc.groupControllers.values.length == 1, "Expected 1, but receive ${boardBloc.groupControllers.values.length}", ); + final expectedGroupName = "No ${multiSelectField.name}"; + assert( + boardBloc.groupControllers.values.first.group.groupName == + expectedGroupName, + "Expected $expectedGroupName, but receive ${boardBloc.groupControllers.values.first.group.groupName}", + ); }); test('group by multi select with no options test', () async { @@ -60,23 +67,22 @@ void main() { final multiSelectField = context.fieldContexts.last.field; // Create options - final cellController = - context.makeCellControllerFromFieldId(multiSelectField.id) - as SelectOptionCellController; + final cellController = await context.makeCellController(multiSelectField.id) + as SelectOptionCellController; - final bloc = SelectOptionCellEditorBloc(cellController: cellController); + final multiSelectOptionBloc = + SelectOptionCellEditorBloc(cellController: cellController); + multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial()); await boardResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A")); await boardResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B")); await boardResponseFuture(); // set grouped by the new multi-select field" final gridGroupBloc = DatabaseGroupBloc( viewId: context.gridView.id, - databaseController: context.databaseController, + fieldController: context.fieldController, )..add(const DatabaseGroupEvent.initial()); await boardResponseFuture(); @@ -90,6 +96,7 @@ void main() { // assert there are only three group final boardBloc = BoardBloc( + view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); @@ -97,5 +104,11 @@ void main() { boardBloc.groupControllers.values.length == 3, "Expected 3, but receive ${boardBloc.groupControllers.values.length}", ); + + final groups = + boardBloc.groupControllers.values.map((e) => e.group).toList(); + assert(groups[0].groupName == "No ${multiSelectField.name}"); + assert(groups[1].groupName == "B"); + assert(groups[2].groupName == "A"); }); } diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart index c68338b424..44a6307e9b 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart @@ -1,6 +1,6 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart'; -import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -18,7 +18,7 @@ void main() { final fieldInfo = context.singleSelectFieldContext(); editorBloc = context.makeFieldEditor( fieldInfo: fieldInfo, - ); + )..add(const FieldEditorEvent.initial()); await boardResponseFuture(); }); @@ -29,15 +29,19 @@ void main() { build: () => editorBloc, wait: boardResponseDuration(), act: (bloc) async { - bloc.add(const FieldEditorEvent.switchFieldType(FieldType.RichText)); + bloc.add(const FieldEditorEvent.switchToField(FieldType.RichText)); }, verify: (bloc) { - assert(bloc.state.field.fieldType == FieldType.RichText); + bloc.state.field.fold( + () => throw Exception(), + (field) => field.fieldType == FieldType.RichText, + ); }, ); blocTest( 'assert the number of groups is 1', build: () => BoardBloc( + view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add( const BoardEvent.initial(), 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..33880ba2bd 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -1,35 +1,38 @@ -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_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/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database/board/board.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/board/board.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import '../../util.dart'; import '../grid_test/util.dart'; class AppFlowyBoardTest { - AppFlowyBoardTest({required this.unitTest}); - final AppFlowyUnitTest unitTest; + AppFlowyBoardTest({required this.unitTest}); + static Future ensureInitialized() async { final inner = await AppFlowyUnitTest.ensureInitialized(); return AppFlowyBoardTest(unitTest: inner); } Future createTestBoard() async { - final app = await unitTest.createWorkspace(); + final app = await unitTest.createTestApp(); final builder = BoardPluginBuilder(); return ViewBackendService.createView( parentViewId: app.id, name: "Test Board", - layoutType: builder.layoutType, + layoutType: builder.layoutType!, openAfterCreate: true, ).then((result) { return result.fold( @@ -51,19 +54,19 @@ class AppFlowyBoardTest { } Future boardResponseFuture() { - return Future.delayed(boardResponseDuration()); + return Future.delayed(boardResponseDuration(milliseconds: 200)); } -Duration boardResponseDuration({int milliseconds = 2000}) { +Duration boardResponseDuration({int milliseconds = 200}) { return Duration(milliseconds: milliseconds); } class BoardTestContext { - BoardTestContext(this.gridView, this._boardDataController); - final ViewPB gridView; final DatabaseController _boardDataController; + BoardTestContext(this.gridView, this._boardDataController); + List get rowInfos { return _boardDataController.rowCache.rowInfos; } @@ -74,30 +77,57 @@ class BoardTestContext { return _boardDataController.fieldController; } - DatabaseController get databaseController => _boardDataController; - FieldEditorBloc makeFieldEditor({ required FieldInfo fieldInfo, - }) => - FieldEditorBloc( - viewId: databaseController.viewId, - fieldController: fieldController, - fieldInfo: fieldInfo, - isNew: false, - ); + }) { + final loader = FieldTypeOptionLoader( + viewId: gridView.id, + field: fieldInfo.field, + ); - CellController makeCellControllerFromFieldId(String fieldId) { - return makeCellController( - _boardDataController, - CellContext(fieldId: fieldId, rowId: rowInfos.last.rowId), + final editorBloc = FieldEditorBloc( + isGroupField: fieldInfo.isGroupField, + loader: loader, + field: fieldInfo.field, + ); + return editorBloc; + } + + Future makeCellController(String fieldId) async { + final builder = await makeCellControllerBuilder(fieldId); + return builder.build(); + } + + Future makeCellControllerBuilder( + String fieldId, + ) async { + final RowInfo rowInfo = rowInfos.last; + final rowCache = _boardDataController.rowCache; + + final rowDataController = RowController( + viewId: rowInfo.viewId, + rowMeta: rowInfo.rowMeta, + rowCache: rowCache, + ); + + final rowBloc = RowBloc( + viewId: rowInfo.viewId, + dataController: rowDataController, + rowId: rowInfo.rowMeta.id, + )..add(const RowEvent.initial()); + await gridResponseFuture(); + + return CellControllerBuilder( + cellContext: rowBloc.state.cellByFieldId[fieldId]!, + cellCache: rowCache.cellCache, ); } Future createField(FieldType fieldType) async { - final editorBloc = - await createFieldEditor(databaseController: _boardDataController); + final editorBloc = await createFieldEditor(viewId: gridView.id) + ..add(const FieldEditorEvent.initial()); await gridResponseFuture(); - editorBloc.add(FieldEditorEvent.switchFieldType(fieldType)); + editorBloc.add(FieldEditorEvent.switchToField(fieldType)); await gridResponseFuture(); return Future(() => editorBloc); } @@ -108,6 +138,11 @@ class BoardTestContext { return fieldInfo; } + FieldContext singleSelectFieldCellContext() { + final field = singleSelectFieldContext().field; + return FieldContext(viewId: gridView.id, field: field); + } + FieldInfo textFieldContext() { final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.RichText); diff --git a/frontend/appflowy_flutter/test/bloc_test/chat_test/chat_load_message_test.dart b/frontend/appflowy_flutter/test/bloc_test/chat_test/chat_load_message_test.dart deleted file mode 100644 index 134a429a6b..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/chat_test/chat_load_message_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'util.dart'; - -void main() { - // ignore: unused_local_variable - late AppFlowyChatTest chatTest; - - setUpAll(() async { - chatTest = await AppFlowyChatTest.ensureInitialized(); - }); - - test('send message', () async { - // final context = await chatTest.createChat(); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart deleted file mode 100644 index ece0c5e027..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:appflowy/plugins/ai_chat/chat.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; - -import '../../util.dart'; - -class AppFlowyChatTest { - AppFlowyChatTest({required this.unitTest}); - - final AppFlowyUnitTest unitTest; - - static Future ensureInitialized() async { - final inner = await AppFlowyUnitTest.ensureInitialized(); - return AppFlowyChatTest(unitTest: inner); - } - - Future createChat() async { - final app = await unitTest.createWorkspace(); - final builder = AIChatPluginBuilder(); - return ViewBackendService.createView( - parentViewId: app.id, - name: "Test Chat", - layoutType: builder.layoutType, - openAfterCreate: true, - ).then((result) { - return result.fold( - (view) async { - return view; - }, - (error) { - throw Exception(); - }, - ); - }); - } -} 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..32c90ba930 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,37 +1,30 @@ -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/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.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:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:dartz/dartz.dart'; 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 = await cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("A")); await gridResponseFuture(); expect(bloc.state.options.length, 1); @@ -39,17 +32,24 @@ void main() { }); test('update options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = await cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("A")); await gridResponseFuture(); final SelectOptionPB optionUpdate = bloc.state.options[0] ..color = SelectOptionColorPB.Aqua ..name = "B"; - bloc.add(SelectOptionCellEditorEvent.updateOption(optionUpdate)); + bloc.add(SelectOptionEditorEvent.updateOption(optionUpdate)); expect(bloc.state.options.length, 1); expect(bloc.state.options[0].name, "B"); @@ -57,63 +57,68 @@ void main() { }); test('delete options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = await cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("A")); await gridResponseFuture(); assert( bloc.state.options.length == 1, "Expect 1 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("B")); await gridResponseFuture(); assert( bloc.state.options.length == 2, "Expect 2 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionCellEditorEvent.filterOption("C")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("C")); await gridResponseFuture(); assert( bloc.state.options.length == 3, "Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}", ); - bloc.add(SelectOptionCellEditorEvent.deleteOption(bloc.state.options[0])); - await gridResponseFuture(); - assert( - bloc.state.options.length == 2, - "Expect 2 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}", - ); - - bloc.add(const SelectOptionCellEditorEvent.deleteAllOptions()); + bloc.add(const SelectOptionEditorEvent.deleteAllOptions()); await gridResponseFuture(); assert( bloc.state.options.isEmpty, - "Expect empty but receive ${bloc.state.options.length}. Options: ${bloc.state.options}", + "Expect empty but receive ${bloc.state.options.length}", ); }); test('select/unselect option', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = await cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("A")); await gridResponseFuture(); final optionId = bloc.state.options[0].id; - bloc.add(SelectOptionCellEditorEvent.unselectOption(optionId)); + bloc.add(SelectOptionEditorEvent.unSelectOption(optionId)); await gridResponseFuture(); assert(bloc.state.selectedOptions.isEmpty); - bloc.add(SelectOptionCellEditorEvent.selectOption(optionId)); + bloc.add(SelectOptionEditorEvent.selectOption(optionId)); await gridResponseFuture(); assert(bloc.state.selectedOptions.length == 1); @@ -121,40 +126,51 @@ void main() { }); test('select an option or create one', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = await cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("A")); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); - bloc.add(const SelectOptionCellEditorEvent.submitTextField()); + bloc.add(const SelectOptionEditorEvent.trySelectOption("B")); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.submitTextField()); + bloc.add(const SelectOptionEditorEvent.trySelectOption("A")); await gridResponseFuture(); - expect(bloc.state.selectedOptions.length, 1); - expect(bloc.state.options.length, 1); + assert(bloc.state.selectedOptions.length == 1); + assert(bloc.state.options.length == 2); expect(bloc.state.selectedOptions[0].name, "A"); }); test('select multiple options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = await cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("A")); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("B")); await gridResponseFuture(); bloc.add( - const SelectOptionCellEditorEvent.selectMultipleOptions( + const SelectOptionEditorEvent.selectMultipleOptions( ["A", "B", "C"], "x", ), @@ -163,15 +179,22 @@ void main() { assert(bloc.state.selectedOptions.length == 1); expect(bloc.state.selectedOptions[0].name, "A"); - expect(bloc.filter, "x"); + expect(bloc.state.filter, const Some("x")); }); test('filter options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = await cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); + bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionCellEditorEvent.filterOption("abcd")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("abcd")); await gridResponseFuture(); expect( bloc.state.options.length, @@ -179,8 +202,7 @@ void main() { reason: "Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionCellEditorEvent.filterOption("aaaa")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("aaaa")); await gridResponseFuture(); expect( bloc.state.options.length, @@ -188,8 +210,7 @@ void main() { reason: "Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionCellEditorEvent.filterOption("defg")); - bloc.add(const SelectOptionCellEditorEvent.createOption()); + bloc.add(const SelectOptionEditorEvent.newOption("defg")); await gridResponseFuture(); expect( bloc.state.options.length, @@ -197,7 +218,7 @@ void main() { reason: "Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionCellEditorEvent.filterOption("a")); + bloc.add(const SelectOptionEditorEvent.filterOption("a")); await gridResponseFuture(); expect( @@ -206,12 +227,12 @@ void main() { reason: "Options: ${bloc.state.options}", ); expect( - bloc.allOptions.length, + bloc.state.allOptions.length, 3, reason: "Options: ${bloc.state.options}", ); - expect(bloc.state.createSelectOptionSuggestion!.name, "a"); - expect(bloc.filter, "a"); + expect(bloc.state.createOption, const Some("a")); + expect(bloc.state.filter, const Some("a")); }); }); } 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..f47e5181ec --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart @@ -0,0 +1,93 @@ +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.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(); + final loader = FieldTypeOptionLoader( + viewId: context.gridView.id, + field: fieldInfo.field, + ); + + return FieldEditorBloc( + isGroupField: fieldInfo.isGroupField, + loader: loader, + field: fieldInfo.field, + )..add(const FieldEditorEvent.initial()); +} + +void main() { + late AppFlowyGridTest gridTest; + + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test('rename field', () async { + final editorBloc = await makeEditorBloc(gridTest); + editorBloc.add(const FieldEditorEvent.updateName('Hello world')); + await gridResponseFuture(); + + editorBloc.state.field.fold( + () => throw Exception("The field should not be none"), + (field) { + assert(field.name == 'Hello world'); + }, + ); + }); + + test('switch to text field', () async { + final editorBloc = await makeEditorBloc(gridTest); + + editorBloc.add(const FieldEditorEvent.switchToField(FieldType.RichText)); + await gridResponseFuture(); + + editorBloc.state.field.fold( + () => throw Exception("The field should not be none"), + (field) { + // The default length of the fields is 3. The length of the fields + // should not change after switching to other field type + // assert(gridTest.fieldContexts.length == 3); + assert(field.fieldType == FieldType.RichText); + }, + ); + }); + + test('delete field', () async { + final editorBloc = await makeEditorBloc(gridTest); + editorBloc.add(const FieldEditorEvent.switchToField(FieldType.RichText)); + await gridResponseFuture(); + + editorBloc.state.field.fold( + () => throw Exception("The field should not be none"), + (field) { + // The default length of the fields is 3. The length of the fields + // should not change after switching to other field type + // assert(gridTest.fieldContexts.length == 3); + assert(field.fieldType == FieldType.RichText); + }, + ); + }); +} + +Future makeEditorBloc(AppFlowyGridTest gridTest) async { + final context = await gridTest.createTestGrid(); + final fieldInfo = context.singleSelectFieldContext(); + final loader = FieldTypeOptionLoader( + viewId: context.gridView.id, + field: fieldInfo.field, + ); + + final editorBloc = FieldEditorBloc( + isGroupField: fieldInfo.isGroupField, + loader: loader, + field: fieldInfo.field, + )..add(const FieldEditorEvent.initial()); + + await gridResponseFuture(); + + return editorBloc; +} 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 deleted file mode 100644 index a1fd6d35cc..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest gridTest; - - setUpAll(() async { - gridTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('field cell bloc:', () { - late GridTestContext context; - late double width; - - setUp(() async { - context = await gridTest.makeDefaultTestGrid(); - }); - - blocTest( - 'update field width', - build: () => FieldCellBloc( - fieldInfo: context.fieldController.fieldInfos[0], - viewId: context.viewId, - ), - act: (bloc) { - width = bloc.state.width; - bloc.add(const FieldCellEvent.onResizeStart()); - bloc.add(const FieldCellEvent.startUpdateWidth(100)); - bloc.add(const FieldCellEvent.endUpdateWidth()); - }, - verify: (bloc) { - expect(bloc.state.width, width + 100); - }, - ); - - blocTest( - 'field width should not be less than 50px', - build: () => FieldCellBloc( - viewId: context.viewId, - fieldInfo: context.fieldController.fieldInfos[0], - ), - act: (bloc) { - bloc.add(const FieldCellEvent.onResizeStart()); - bloc.add(const FieldCellEvent.startUpdateWidth(-110)); - bloc.add(const FieldCellEvent.endUpdateWidth()); - }, - verify: (bloc) { - expect(bloc.state.width, 50); - }, - ); - }); -} 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..c823639792 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart @@ -0,0 +1,161 @@ +import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database_view/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, + fieldType: textField.fieldType, + ); + 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(); + 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 = await context.makeTextCellController(0); + controller.saveCellData("edit text cell content"); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 2); + + 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..9f3cf6ff2d --- /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_view/application/filter/filter_service.dart'; +import 'package:appflowy/plugins/database_view/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 = GridFilterMenuBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const GridFilterMenuEvent.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 == 2); + }); + + test('test filter menu after update existing text filter)', () async { + final context = await gridTest.createTestGrid(); + final menuBloc = GridFilterMenuBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const GridFilterMenuEvent.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.Is, + content: "ABC", + ); + await gridResponseFuture(); + assert( + menuBloc.state.filters.first.textFilter()!.condition == + TextFilterConditionPB.Is, + ); + 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..a56c70f326 --- /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_view/application/filter/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 = await context.makeCheckboxCellController(0); + 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 = await context.makeCheckboxCellController(0); + 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..f28e27a5b9 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart @@ -0,0 +1,173 @@ +import 'package:appflowy/plugins/database_view/application/filter/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, + fieldType: textField.fieldType, + ); + 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, + fieldType: textField.fieldType, + ); + 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, + fieldType: textField.fieldType, + ); + 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.Is, + 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.Is, + 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.Is, + 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.Is, + content: "C", + ); + await gridResponseFuture(); + assert(context.rowInfos.isEmpty); + + // delete the filter + await service.deleteFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + fieldType: textField.fieldType, + ); + 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..826a191bf2 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart @@ -0,0 +1,42 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart'; + +import '../util.dart'; + +Future createTestFilterGrid(AppFlowyGridTest gridTest) async { + final app = await gridTest.unitTest.createTestApp(); + 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); + await gridResponseFuture(milliseconds: 500); + result.fold((l) => null, (r) => throw Exception(r)); + return context; + }, + (error) => throw Exception(), + ); + }); + + return context; +} + +Future editCells(GridTestContext context) async { + final controller0 = await context.makeTextCellController(0); + final controller1 = await context.makeTextCellController(1); + + controller0.saveCellData('A'); + await gridResponseFuture(); + controller1.saveCellData('B'); +} 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..e7fa5cc464 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 @@ -1,13 +1,11 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; -import 'package:bloc_test/bloc_test.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:bloc_test/bloc_test.dart'; import 'util.dart'; void main() { late AppFlowyGridTest gridTest; - setUpAll(() async { gridTest = await AppFlowyGridTest.ensureInitialized(); }); @@ -16,37 +14,39 @@ 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 - // We create one row so we expect 4 rows + // The initial number of rows is 3 for each grid. 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 +57,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 +69,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/grid_header_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_header_bloc_test.dart new file mode 100644 index 0000000000..571cb11d89 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_header_bloc_test.dart @@ -0,0 +1,127 @@ +import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + group('$GridHeaderBloc', () { + late FieldActionSheetBloc actionSheetBloc; + late GridTestContext context; + setUp(() async { + context = await gridTest.createTestGrid(); + actionSheetBloc = FieldActionSheetBloc( + fieldCellContext: context.singleSelectFieldCellContext(), + ); + }); + + blocTest( + "hides property", + build: () { + final bloc = GridHeaderBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const GridHeaderEvent.initial()); + return bloc; + }, + act: (bloc) async { + actionSheetBloc.add(const FieldActionSheetEvent.hideField()); + await Future.delayed(gridResponseDuration()); + }, + wait: gridResponseDuration(), + verify: (bloc) { + assert(bloc.state.fields.length == 2); + }, + ); + + blocTest( + "shows property", + build: () { + final bloc = GridHeaderBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const GridHeaderEvent.initial()); + return bloc; + }, + act: (bloc) async { + actionSheetBloc.add(const FieldActionSheetEvent.hideField()); + await Future.delayed(gridResponseDuration()); + actionSheetBloc.add(const FieldActionSheetEvent.showField()); + await Future.delayed(gridResponseDuration()); + }, + wait: gridResponseDuration(), + verify: (bloc) { + assert(bloc.state.fields.length == 3); + }, + ); + + blocTest( + "duplicate property", + build: () { + final bloc = GridHeaderBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const GridHeaderEvent.initial()); + return bloc; + }, + act: (bloc) async { + actionSheetBloc.add(const FieldActionSheetEvent.duplicateField()); + await Future.delayed(gridResponseDuration()); + }, + wait: gridResponseDuration(), + verify: (bloc) { + expect(bloc.state.fields.length, 4); + }, + ); + + blocTest( + "delete property", + build: () { + final bloc = GridHeaderBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const GridHeaderEvent.initial()); + return bloc; + }, + act: (bloc) async { + actionSheetBloc.add(const FieldActionSheetEvent.deleteField()); + await Future.delayed(gridResponseDuration()); + }, + wait: gridResponseDuration(), + verify: (bloc) { + expect(bloc.state.fields.length, 2); + }, + ); + + blocTest( + "update name", + build: () { + final bloc = GridHeaderBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const GridHeaderEvent.initial()); + return bloc; + }, + act: (bloc) async { + actionSheetBloc + .add(const FieldActionSheetEvent.updateFieldName("Hello world")); + await Future.delayed(gridResponseDuration()); + }, + wait: gridResponseDuration(), + verify: (bloc) { + final field = bloc.state.fields.firstWhere( + (element) => element.id == actionSheetBloc.fieldService.fieldId, + ); + + expect(field.name, "Hello world"); + }, + ); + }); +} 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..c3ca8408eb 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -1,72 +1,157 @@ -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/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_view/application/cell/cell_controller.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.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-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/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 'package:dartz/dartz.dart'; import '../../util.dart'; -const v020GridFileName = "v020.afdb"; -const v069GridFileName = "v069.afdb"; - class GridTestContext { - GridTestContext(this.view, this.databaseController); + final ViewPB gridView; + final DatabaseController gridController; - final ViewPB view; - final DatabaseController databaseController; - - String get viewId => view.id; + GridTestContext(this.gridView, this.gridController); List get rowInfos { - return databaseController.rowCache.rowInfos; + return gridController.rowCache.rowInfos; } - FieldController get fieldController => databaseController.fieldController; + List get fieldContexts => fieldController.fieldInfos; + + FieldController get fieldController { + return gridController.fieldController; + } + + Future> createRow() async { + return gridController.createRow(); + } + + Future makeCellController( + String fieldId, + int rowIndex, + ) async { + final builder = await makeCellControllerBuilder(fieldId, rowIndex); + return builder.build(); + } + + Future makeCellControllerBuilder( + String fieldId, + int rowIndex, + ) async { + final RowInfo rowInfo = rowInfos[rowIndex]; + final rowCache = gridController.rowCache; + + final rowDataController = RowController( + rowMeta: rowInfo.rowMeta, + viewId: rowInfo.viewId, + rowCache: rowCache, + ); + + final rowBloc = RowBloc( + viewId: rowInfo.viewId, + dataController: rowDataController, + rowId: rowInfo.rowMeta.id, + )..add(const RowEvent.initial()); + await gridResponseFuture(); + + return CellControllerBuilder( + cellContext: rowBloc.state.cellByFieldId[fieldId]!, + cellCache: rowCache.cellCache, + ); + } Future createField(FieldType fieldType) async { - final editorBloc = - await createFieldEditor(databaseController: databaseController); + final editorBloc = await createFieldEditor(viewId: gridView.id) + ..add(const FieldEditorEvent.initial()); await gridResponseFuture(); - editorBloc.add(FieldEditorEvent.switchFieldType(fieldType)); + editorBloc.add(FieldEditorEvent.switchToField(fieldType)); await gridResponseFuture(); - return editorBloc; + return Future(() => editorBloc); } - CellController makeGridCellController(int fieldIndex, int rowIndex) { - return makeCellController( - databaseController, - CellContext( - fieldId: fieldController.fieldInfos[fieldIndex].id, - rowId: rowInfos[rowIndex].rowId, - ), - ).as(); + FieldInfo singleSelectFieldContext() { + final fieldInfo = fieldContexts + .firstWhere((element) => element.fieldType == FieldType.SingleSelect); + return fieldInfo; + } + + FieldContext singleSelectFieldCellContext() { + final field = singleSelectFieldContext().field; + return FieldContext(viewId: gridView.id, field: field); + } + + FieldInfo textFieldContext() { + final fieldInfo = fieldContexts + .firstWhere((element) => element.fieldType == FieldType.RichText); + return fieldInfo; + } + + FieldInfo checkboxFieldContext() { + final fieldInfo = fieldContexts + .firstWhere((element) => element.fieldType == FieldType.Checkbox); + return fieldInfo; + } + + Future makeSelectOptionCellController( + FieldType fieldType, + int rowIndex, + ) async { + assert( + fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect, + ); + + final field = + fieldContexts.firstWhere((element) => element.fieldType == fieldType); + final cellController = await makeCellController(field.id, rowIndex) + as SelectOptionCellController; + return cellController; + } + + Future makeTextCellController(int rowIndex) async { + final field = fieldContexts + .firstWhere((element) => element.fieldType == FieldType.RichText); + final cellController = + await makeCellController(field.id, rowIndex) as TextCellController; + return cellController; + } + + Future makeCheckboxCellController(int rowIndex) async { + final field = fieldContexts + .firstWhere((element) => element.fieldType == FieldType.Checkbox); + final cellController = + await makeCellController(field.id, rowIndex) as TextCellController; + return cellController; } } Future createFieldEditor({ - required DatabaseController databaseController, + required String viewId, }) async { - final result = await FieldBackendService.createField( - viewId: databaseController.viewId, + final result = await TypeOptionBackendService.createFieldTypeOption( + viewId: viewId, ); - await gridResponseFuture(); return result.fold( - (field) { + (data) { + final loader = FieldTypeOptionLoader( + viewId: viewId, + field: data.field_2, + ); return FieldEditorBloc( - viewId: databaseController.viewId, - fieldController: databaseController.fieldController, - fieldInfo: databaseController.fieldController.getField(field.id)!, - isNew: true, + isGroupField: FieldInfo(field: data.field_2).isGroupField, + loader: loader, + field: data.field_2, ); }, (err) => throw Exception(err), @@ -75,83 +160,74 @@ Future createFieldEditor({ /// Create a empty Grid for test class AppFlowyGridTest { - AppFlowyGridTest({required this.unitTest}); - final AppFlowyUnitTest unitTest; + AppFlowyGridTest({required this.unitTest}); + static Future ensureInitialized() async { final inner = await AppFlowyUnitTest.ensureInitialized(); return AppFlowyGridTest(unitTest: inner); } - Future makeDefaultTestGrid() async { - final workspace = await unitTest.createWorkspace(); + Future createTestGrid() async { + final app = await unitTest.createTestApp(); 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(), - ); + ).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 makeTestGridFromImportedData( - String fileName, +/// Create a new Grid for cell test +class AppFlowyGridCellTest { + late GridTestContext context; + final AppFlowyGridTest gridTest; + AppFlowyGridCellTest({required this.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 context.createRow(); + } + + Future makeSelectOptionCellController( + FieldType fieldType, + int rowIndex, ) 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(), - ); - - return context; + return await context.makeSelectOptionCellController(fieldType, rowIndex); } } -Future gridResponseFuture({int milliseconds = 300}) { - return Future.delayed( - gridResponseDuration(milliseconds: milliseconds), - ); +Future gridResponseFuture({int milliseconds = 200}) { + return Future.delayed(gridResponseDuration(milliseconds: milliseconds)); } -Duration gridResponseDuration({int milliseconds = 300}) { +Duration gridResponseDuration({int milliseconds = 200}) { return Duration(milliseconds: milliseconds); } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart new file mode 100644 index 0000000000..8913709d4c --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart @@ -0,0 +1,166 @@ +import 'package:appflowy/plugins/document/application/doc_bloc.dart'; +import 'package:appflowy/workspace/application/app/app_bloc.dart'; +import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../util.dart'; + +void main() { + late AppFlowyUnitTest testContext; + setUpAll(() async { + testContext = await AppFlowyUnitTest.ensureInitialized(); + }); + + test('rename app test', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.rename('Hello world')); + await blocResponseFuture(); + + expect(bloc.state.view.name, 'Hello world'); + }); + + test('delete app test', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.delete()); + await blocResponseFuture(); + + final apps = await testContext.loadApps(); + expect(apps.where((element) => element.id == app.id).isEmpty, true); + }); + + test('create documents in order', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document)); + await blocResponseFuture(); + bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document)); + await blocResponseFuture(); + bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document)); + await blocResponseFuture(); + + assert(bloc.state.views[0].name == '1'); + assert(bloc.state.views[1].name == '2'); + assert(bloc.state.views[2].name == '3'); + }); + + test('reorder documents test', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document)); + await blocResponseFuture(); + bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document)); + await blocResponseFuture(); + bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document)); + await blocResponseFuture(); + assert(bloc.state.views.length == 3); + + final appViewData = ViewDataContext(viewId: app.id); + appViewData.views = bloc.state.views; + + final viewSectionBloc = ViewSectionBloc( + appViewData: appViewData, + )..add(const ViewSectionEvent.initial()); + await blocResponseFuture(); + + viewSectionBloc.add(const ViewSectionEvent.moveView(0, 2)); + await blocResponseFuture(); + + assert(bloc.state.views[0].name == '2'); + assert(bloc.state.views[1].name == '3'); + assert(bloc.state.views[2].name == '1'); + }); + + test('open latest view test', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + assert( + bloc.state.latestCreatedView == null, + "assert initial latest create view is null after initialize", + ); + + bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document)); + await blocResponseFuture(); + assert( + bloc.state.latestCreatedView!.id == bloc.state.views.last.id, + "create a view and assert the latest create view is this view", + ); + + bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document)); + await blocResponseFuture(); + assert( + bloc.state.latestCreatedView!.id == bloc.state.views.last.id, + "create a view and assert the latest create view is this view", + ); + }); + + test('open latest documents test', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document)); + await blocResponseFuture(); + final document1 = bloc.state.latestCreatedView; + assert(document1!.name == "document 1"); + + bloc.add(const AppEvent.createView("document 2", ViewLayoutPB.Document)); + await blocResponseFuture(); + final document2 = bloc.state.latestCreatedView; + assert(document2!.name == "document 2"); + + // Open document 1 + // ignore: unused_local_variable + final documentBloc = DocumentBloc(view: document1!) + ..add(const DocumentEvent.initial()); + await blocResponseFuture(); + + final workspaceSetting = await FolderEventGetCurrentWorkspace() + .send() + .then((result) => result.fold((l) => l, (r) => throw Exception())); + workspaceSetting.latestView.id == document1.id; + }); + + test('open latest document test', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document)); + await blocResponseFuture(); + final document = bloc.state.latestCreatedView; + assert(document!.name == "document 1"); + + bloc.add(const AppEvent.createView("grid 2", ViewLayoutPB.Grid)); + await blocResponseFuture(); + final grid = bloc.state.latestCreatedView; + assert(grid!.name == "grid 2"); + + var workspaceSetting = await FolderEventGetCurrentWorkspace() + .send() + .then((result) => result.fold((l) => l, (r) => throw Exception())); + workspaceSetting.latestView.id == grid!.id; + + // Open grid 1 + // ignore: unused_local_variable + final documentBloc = DocumentBloc(view: document!) + ..add(const DocumentEvent.initial()); + await blocResponseFuture(); + + workspaceSetting = await FolderEventGetCurrentWorkspace() + .send() + .then((result) => result.fold((l) => l, (r) => throw Exception())); + workspaceSetting.latestView.id == document.id; + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart new file mode 100644 index 0000000000..3e4d092afe --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/workspace/application/app/app_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../util.dart'; + +void main() { + late AppFlowyUnitTest testContext; + setUpAll(() async { + testContext = await AppFlowyUnitTest.ensureInitialized(); + }); + + test('create a document', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("Test document", ViewLayoutPB.Document)); + await blocResponseFuture(); + + assert(bloc.state.views.length == 1); + assert(bloc.state.views.last.name == "Test document"); + assert(bloc.state.views.last.layout == ViewLayoutPB.Document); + }); + + test('create a grid', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("Test grid", ViewLayoutPB.Grid)); + await blocResponseFuture(); + + assert(bloc.state.views.length == 1); + assert(bloc.state.views.last.name == "Test grid"); + assert(bloc.state.views.last.layout == ViewLayoutPB.Grid); + }); + + test('create a kanban', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("Test board", ViewLayoutPB.Board)); + await blocResponseFuture(); + + assert(bloc.state.views.length == 1); + assert(bloc.state.views.last.name == "Test board"); + assert(bloc.state.views.last.layout == ViewLayoutPB.Board); + }); + + test('create a calendar', () async { + final app = await testContext.createTestApp(); + final bloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + bloc.add(const AppEvent.createView("Test calendar", ViewLayoutPB.Calendar)); + await blocResponseFuture(); + + assert(bloc.state.views.length == 1); + assert(bloc.state.views.last.name == "Test calendar"); + assert(bloc.state.views.last.layout == ViewLayoutPB.Calendar); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart index 6a34bc68d0..7d9f158b69 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart @@ -1,8 +1,8 @@ -import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/plugins/document/application/doc_bloc.dart'; +import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy/workspace/application/home/home_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../util.dart'; @@ -13,49 +13,45 @@ void main() { testContext = await AppFlowyUnitTest.ensureInitialized(); }); - test('init home screen', () async { - final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting() + test('initi home screen', () async { + final workspaceSetting = await FolderEventGetCurrentWorkspace() .send() .then((result) => result.fold((l) => l, (r) => throw Exception())); await blocResponseFuture(); - final homeBloc = HomeBloc(workspaceSetting)..add(const HomeEvent.initial()); + final homeBloc = HomeBloc(testContext.userProfile, workspaceSetting) + ..add(const HomeEvent.initial()); await blocResponseFuture(); assert(homeBloc.state.workspaceSetting.hasLatestView()); }); test('open the document', () async { - final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting() + final workspaceSetting = await FolderEventGetCurrentWorkspace() .send() .then((result) => result.fold((l) => l, (r) => throw Exception())); await blocResponseFuture(); - final homeBloc = HomeBloc(workspaceSetting)..add(const HomeEvent.initial()); + final homeBloc = HomeBloc(testContext.userProfile, workspaceSetting) + ..add(const HomeEvent.initial()); await blocResponseFuture(); - final app = await testContext.createWorkspace(); - final appBloc = ViewBloc(view: app)..add(const ViewEvent.initial()); - assert(appBloc.state.lastCreatedView == null); + final app = await testContext.createTestApp(); + final appBloc = AppBloc(view: app)..add(const AppEvent.initial()); + assert(appBloc.state.latestCreatedView == null); - appBloc.add( - const ViewEvent.createView( - "New document", - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); + appBloc + .add(const AppEvent.createView("New document", ViewLayoutPB.Document)); await blocResponseFuture(); - assert(appBloc.state.lastCreatedView != null); - final latestView = appBloc.state.lastCreatedView!; - final _ = DocumentBloc(documentId: latestView.id) + assert(appBloc.state.latestCreatedView != null); + final latestView = appBloc.state.latestCreatedView!; + final _ = DocumentBloc(view: latestView) ..add(const DocumentEvent.initial()); await FolderEventSetLatestView(ViewIdPB(value: latestView.id)).send(); await blocResponseFuture(); - final actual = homeBloc.state.workspaceSetting.latestView.id; - assert(actual == latestView.id); + assert(homeBloc.state.workspaceSetting.latestView.id == latestView.id); }); } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart new file mode 100644 index 0000000000..2f0e94f346 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart @@ -0,0 +1,42 @@ +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../util.dart'; + +void main() { + late AppFlowyUnitTest testContext; + setUpAll(() async { + testContext = await AppFlowyUnitTest.ensureInitialized(); + }); + + test('assert initial apps is the build-in app', () async { + final menuBloc = MenuBloc( + user: testContext.userProfile, + workspace: testContext.currentWorkspace, + )..add(const MenuEvent.initial()); + await blocResponseFuture(); + + assert(menuBloc.state.views.length == 1); + }); + + test('reorder apps', () async { + final menuBloc = MenuBloc( + user: testContext.userProfile, + workspace: testContext.currentWorkspace, + )..add(const MenuEvent.initial()); + await blocResponseFuture(); + menuBloc.add(const MenuEvent.createApp("App 1")); + await blocResponseFuture(); + menuBloc.add(const MenuEvent.createApp("App 2")); + await blocResponseFuture(); + menuBloc.add(const MenuEvent.createApp("App 3")); + await blocResponseFuture(); + + menuBloc.add(const MenuEvent.moveApp(1, 3)); + await blocResponseFuture(); + + assert(menuBloc.state.views[1].name == 'App 2'); + assert(menuBloc.state.views[2].name == 'App 3'); + assert(menuBloc.state.views[3].name == 'App 1'); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart deleted file mode 100644 index 75ade70a87..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../util.dart'; - -void main() { - late AppFlowyUnitTest testContext; - setUpAll(() async { - testContext = await AppFlowyUnitTest.ensureInitialized(); - }); - - test('assert initial apps is the build-in app', () async { - final menuBloc = SidebarSectionsBloc() - ..add( - SidebarSectionsEvent.initial( - testContext.userProfile, - testContext.currentWorkspace.id, - ), - ); - - await blocResponseFuture(); - - assert(menuBloc.state.section.publicViews.length == 1); - assert(menuBloc.state.section.privateViews.isEmpty); - }); - - test('create views', () async { - final menuBloc = SidebarSectionsBloc() - ..add( - SidebarSectionsEvent.initial( - testContext.userProfile, - testContext.currentWorkspace.id, - ), - ); - await blocResponseFuture(); - - final names = ['View 1', 'View 2', 'View 3']; - for (final name in names) { - menuBloc.add( - SidebarSectionsEvent.createRootViewInSection( - name: name, - index: 0, - viewSection: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(); - } - - final reversedNames = names.reversed.toList(); - for (var i = 0; i < names.length; i++) { - assert( - menuBloc.state.section.publicViews[i].name == reversedNames[i], - ); - } - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart index eed8f177b2..599877fac2 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart @@ -1,51 +1,48 @@ import 'package:appflowy/plugins/trash/application/trash_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy/workspace/application/app/app_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../util.dart'; class TrashTestContext { - TrashTestContext(this.unitTest); - late ViewPB view; - late ViewBloc viewBloc; + late AppBloc appBloc; late List allViews; final AppFlowyUnitTest unitTest; + TrashTestContext(this.unitTest); + Future initialize() async { - view = await unitTest.createWorkspace(); - viewBloc = ViewBloc(view: view)..add(const ViewEvent.initial()); + view = await unitTest.createTestApp(); + appBloc = AppBloc(view: view)..add(const AppEvent.initial()); await blocResponseFuture(); - viewBloc.add( - const ViewEvent.createView( + appBloc.add( + const AppEvent.createView( "Document 1", ViewLayoutPB.Document, - section: ViewSectionPB.Public, ), ); - await blocResponseFuture(millisecond: 300); + await blocResponseFuture(); - viewBloc.add( - const ViewEvent.createView( + appBloc.add( + const AppEvent.createView( "Document 2", ViewLayoutPB.Document, - section: ViewSectionPB.Public, ), ); - await blocResponseFuture(millisecond: 300); + await blocResponseFuture(); - viewBloc.add( - const ViewEvent.createView( + appBloc.add( + const AppEvent.createView( "Document 3", ViewLayoutPB.Document, - section: ViewSectionPB.Public, ), ); - await blocResponseFuture(millisecond: 300); + await blocResponseFuture(); - allViews = [...viewBloc.state.view.childViews]; + allViews = [...appBloc.state.view.childViews]; assert(allViews.length == 3, 'but receive ${allViews.length}'); } } @@ -67,32 +64,26 @@ void main() { final context = TrashTestContext(unitTest); await context.initialize(); final trashBloc = TrashBloc()..add(const TrashEvent.initial()); - await blocResponseFuture(); + await blocResponseFuture(millisecond: 200); // delete a view - final deletedView = context.viewBloc.state.view.childViews[0]; - final deleteViewBloc = ViewBloc(view: deletedView) - ..add(const ViewEvent.initial()); + final deletedView = context.appBloc.state.view.childViews[0]; + context.appBloc.add(AppEvent.deleteView(deletedView.id)); await blocResponseFuture(); - deleteViewBloc.add(const ViewEvent.delete()); - await blocResponseFuture(millisecond: 1000); - assert(context.viewBloc.state.view.childViews.length == 2); + assert(context.appBloc.state.view.childViews.length == 2); assert(trashBloc.state.objects.length == 1); assert(trashBloc.state.objects.first.id == deletedView.id); // put back trashBloc.add(TrashEvent.putback(deletedView.id)); - await blocResponseFuture(millisecond: 1000); - assert(context.viewBloc.state.view.childViews.length == 3); + await blocResponseFuture(); + assert(context.appBloc.state.view.childViews.length == 3); assert(trashBloc.state.objects.isEmpty); // delete all views for (final view in context.allViews) { - final deleteViewBloc = ViewBloc(view: view) - ..add(const ViewEvent.initial()); + context.appBloc.add(AppEvent.deleteView(view.id)); await blocResponseFuture(); - deleteViewBloc.add(const ViewEvent.delete()); - await blocResponseFuture(millisecond: 1000); } expect(trashBloc.state.objects[0].id, context.allViews[0].id); expect(trashBloc.state.objects[1].id, context.allViews[1].id); @@ -100,12 +91,12 @@ void main() { // delete a view permanently trashBloc.add(TrashEvent.delete(trashBloc.state.objects[0])); - await blocResponseFuture(millisecond: 1000); + await blocResponseFuture(); expect(trashBloc.state.objects.length, 2); // delete all view permanently trashBloc.add(const TrashEvent.deleteAll()); - await blocResponseFuture(millisecond: 1000); + await blocResponseFuture(); assert( trashBloc.state.objects.isEmpty, "but receive ${trashBloc.state.objects.length}", 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..d0da0bfb49 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 @@ -1,231 +1,72 @@ -import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../util.dart'; void main() { - const name = 'Hello world'; - late AppFlowyUnitTest testContext; - setUpAll(() async { testContext = await AppFlowyUnitTest.ensureInitialized(); }); - Future createTestViewBloc() async { - final view = await testContext.createWorkspace(); - final viewBloc = ViewBloc(view: view) - ..add( - const ViewEvent.initial(), - ); - await blocResponseFuture(); - return viewBloc; - } - test('rename view test', () async { - final viewBloc = await createTestViewBloc(); - viewBloc.add(const ViewEvent.rename(name)); + final app = await testContext.createTestApp(); + + final appBloc = AppBloc(view: app)..add(const AppEvent.initial()); + appBloc.add( + const AppEvent.createView("Test document", ViewLayoutPB.Document), + ); + await blocResponseFuture(); - expect(viewBloc.state.view.name, name); + + final viewBloc = ViewBloc(view: appBloc.state.views.first) + ..add(const ViewEvent.initial()); + viewBloc.add(const ViewEvent.rename('Hello world')); + await blocResponseFuture(); + + assert(viewBloc.state.view.name == "Hello world"); }); test('duplicate view test', () async { - final viewBloc = await createTestViewBloc(); - // create a nested view - viewBloc.add( - const ViewEvent.createView( - name, - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), + final app = await testContext.createTestApp(); + final appBloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + appBloc.add( + const AppEvent.createView("Test document", ViewLayoutPB.Document), ); await blocResponseFuture(); - expect(viewBloc.state.view.childViews.length, 1); - final childViewBloc = ViewBloc(view: viewBloc.state.view.childViews.first) - ..add( - const ViewEvent.initial(), - ); - childViewBloc.add(const ViewEvent.duplicate()); - await blocResponseFuture(millisecond: 1000); - expect(viewBloc.state.view.childViews.length, 2); + + final viewBloc = ViewBloc(view: appBloc.state.views.first) + ..add(const ViewEvent.initial()); + await blocResponseFuture(); + + viewBloc.add(const ViewEvent.duplicate()); + await blocResponseFuture(); + + expect(appBloc.state.views.length, 2); }); test('delete view test', () async { - final viewBloc = await createTestViewBloc(); - viewBloc.add( - const ViewEvent.createView( - name, - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), + final app = await testContext.createTestApp(); + final appBloc = AppBloc(view: app)..add(const AppEvent.initial()); + await blocResponseFuture(); + + appBloc.add( + const AppEvent.createView("Test document", ViewLayoutPB.Document), ); await blocResponseFuture(); - expect(viewBloc.state.view.childViews.length, 1); - final childViewBloc = ViewBloc(view: viewBloc.state.view.childViews.first) - ..add( - const ViewEvent.initial(), - ); - await blocResponseFuture(); - childViewBloc.add(const ViewEvent.delete()); - await blocResponseFuture(); - assert(viewBloc.state.view.childViews.isEmpty); - }); + expect(appBloc.state.views.length, 1); - test('create nested view test', () async { - final viewBloc = await createTestViewBloc(); - viewBloc.add( - const ViewEvent.createView( - 'Document 1', - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(); - final document1Bloc = ViewBloc(view: viewBloc.state.view.childViews.first) - ..add( - const ViewEvent.initial(), - ); - await blocResponseFuture(); - const name = 'Document 1 - 1'; - document1Bloc.add( - const ViewEvent.createView( - 'Document 1 - 1', - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(); - expect(document1Bloc.state.view.childViews.length, 1); - expect(document1Bloc.state.view.childViews.first.name, name); - }); - - test('create documents in order', () async { - final viewBloc = await createTestViewBloc(); - final names = ['1', '2', '3']; - for (final name in names) { - viewBloc.add( - ViewEvent.createView( - name, - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(millisecond: 400); - } - - expect(viewBloc.state.view.childViews.length, 3); - for (var i = 0; i < names.length; i++) { - expect(viewBloc.state.view.childViews[i].name, names[i]); - } - }); - - test('open latest view test', () async { - final viewBloc = await createTestViewBloc(); - expect(viewBloc.state.lastCreatedView, isNull); - - viewBloc.add( - const ViewEvent.createView( - '1', - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(); - expect( - viewBloc.state.lastCreatedView!.id, - viewBloc.state.view.childViews.last.id, - ); - expect( - viewBloc.state.lastCreatedView!.name, - '1', - ); - - viewBloc.add( - const ViewEvent.createView( - '2', - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(); - expect( - viewBloc.state.lastCreatedView!.name, - '2', - ); - }); - - test('open latest document test', () async { - const name1 = 'document'; - final viewBloc = await createTestViewBloc(); - viewBloc.add( - const ViewEvent.createView( - name1, - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(); - final document = viewBloc.state.lastCreatedView!; - assert(document.name == name1); - - const gird = 'grid'; - viewBloc.add( - const ViewEvent.createView( - gird, - ViewLayoutPB.Document, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(); - assert(viewBloc.state.lastCreatedView!.name == gird); - - var workspaceLatest = - await FolderEventGetCurrentWorkspaceSetting().send().then( - (result) => result.fold( - (l) => l, - (r) => throw Exception(), - ), - ); - workspaceLatest.latestView.id == viewBloc.state.lastCreatedView!.id; - - // ignore: unused_local_variable - final documentBloc = DocumentBloc(documentId: document.id) - ..add( - const DocumentEvent.initial(), - ); + final viewBloc = ViewBloc(view: appBloc.state.views.first) + ..add(const ViewEvent.initial()); await blocResponseFuture(); - workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then( - (result) => result.fold( - (l) => l, - (r) => throw Exception(), - ), - ); - workspaceLatest.latestView.id == document.id; - }); + viewBloc.add(const ViewEvent.delete()); + await blocResponseFuture(); - test('create views', () async { - final viewBloc = await createTestViewBloc(); - const layouts = ViewLayoutPB.values; - for (var i = 0; i < layouts.length; i++) { - final layout = layouts[i]; - if (layout == ViewLayoutPB.Chat) { - continue; - } - viewBloc.add( - ViewEvent.createView( - 'Test $layout', - layout, - section: ViewSectionPB.Public, - ), - ); - await blocResponseFuture(millisecond: 1000); - expect(viewBloc.state.view.childViews.length, i + 1); - expect(viewBloc.state.view.childViews.last.name, 'Test $layout'); - expect(viewBloc.state.view.childViews.last.layout, layout); - } + assert(appBloc.state.views.isEmpty); }); } 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 deleted file mode 100644 index ce57c61bd7..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:ffi'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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'; -import 'package:flutter_test/flutter_test.dart'; -// ignore: depend_on_referenced_packages -import 'package:mocktail/mocktail.dart'; - -class MockSettingsShortcutService extends Mock - implements SettingsShortcutService {} - -void main() { - group("ShortcutsCubit", () { - late SettingsShortcutService service; - late ShortcutsCubit shortcutsCubit; - - setUp(() async { - service = MockSettingsShortcutService(); - when( - () => service.saveAllShortcuts(any()), - ).thenAnswer((_) async => true); - when( - () => service.getCustomizeShortcuts(), - ).thenAnswer((_) async => []); - when( - () => service.updateCommandShortcuts(any(), any()), - ).thenAnswer((_) async => Void); - - shortcutsCubit = ShortcutsCubit(service); - }); - - test('initial state is correct', () { - final shortcutsCubit = ShortcutsCubit(service); - expect(shortcutsCubit.state, const ShortcutsState()); - }); - - group('fetchShortcuts', () { - blocTest( - 'calls getCustomizeShortcuts() once', - build: () => shortcutsCubit, - act: (cubit) => cubit.fetchShortcuts(), - verify: (_) { - verify(() => service.getCustomizeShortcuts()).called(1); - }, - ); - - blocTest( - 'emits [updating, failure] when getCustomizeShortcuts() throws', - setUp: () { - when( - () => service.getCustomizeShortcuts(), - ).thenThrow(Exception('oops')); - }, - build: () => shortcutsCubit, - act: (cubit) => cubit.fetchShortcuts(), - expect: () => [ - const ShortcutsState(status: ShortcutsStatus.updating), - isA() - .having((w) => w.status, 'status', ShortcutsStatus.failure), - ], - ); - - blocTest( - 'emits [updating, success] when getCustomizeShortcuts() returns shortcuts', - build: () => shortcutsCubit, - act: (cubit) => cubit.fetchShortcuts(), - expect: () => [ - const ShortcutsState(status: ShortcutsStatus.updating), - isA() - .having((w) => w.status, 'status', ShortcutsStatus.success) - .having( - (w) => w.commandShortcutEvents, - 'shortcuts', - commandShortcutEvents, - ), - ], - ); - }); - - group('updateShortcut', () { - blocTest( - 'calls saveAllShortcuts() once', - build: () => shortcutsCubit, - act: (cubit) => cubit.updateAllShortcuts(), - verify: (_) { - verify(() => service.saveAllShortcuts(any())).called(1); - }, - ); - - blocTest( - 'emits [updating, failure] when saveAllShortcuts() throws', - setUp: () { - when( - () => service.saveAllShortcuts(any()), - ).thenThrow(Exception('oops')); - }, - build: () => shortcutsCubit, - act: (cubit) => cubit.updateAllShortcuts(), - expect: () => [ - const ShortcutsState(status: ShortcutsStatus.updating), - isA() - .having((w) => w.status, 'status', ShortcutsStatus.failure), - ], - ); - - blocTest( - 'emits [updating, success] when saveAllShortcuts() is successful', - build: () => shortcutsCubit, - act: (cubit) => cubit.updateAllShortcuts(), - expect: () => [ - const ShortcutsState(status: ShortcutsStatus.updating), - isA() - .having((w) => w.status, 'status', ShortcutsStatus.success), - ], - ); - }); - - group('resetToDefault', () { - blocTest( - 'calls saveAllShortcuts() once', - build: () => shortcutsCubit, - act: (cubit) => cubit.resetToDefault(), - verify: (_) { - verify(() => service.saveAllShortcuts(any())).called(1); - verify(() => service.getCustomizeShortcuts()).called(1); - }, - ); - - blocTest( - 'emits [updating, failure] when saveAllShortcuts() throws', - setUp: () { - when( - () => service.saveAllShortcuts(any()), - ).thenThrow(Exception('oops')); - }, - build: () => shortcutsCubit, - act: (cubit) => cubit.resetToDefault(), - expect: () => [ - const ShortcutsState(status: ShortcutsStatus.updating), - isA() - .having((w) => w.status, 'status', ShortcutsStatus.failure), - ], - ); - - blocTest( - 'emits [updating, success] when getCustomizeShortcuts() returns shortcuts', - build: () => shortcutsCubit, - act: (cubit) => cubit.resetToDefault(), - expect: () => [ - const ShortcutsState(status: ShortcutsStatus.updating), - isA() - .having((w) => w.status, 'status', ShortcutsStatus.success) - .having( - (w) => w.commandShortcutEvents, - 'shortcuts', - commandShortcutEvents, - ), - ], - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/algorithm/levenshtein_test.dart b/frontend/appflowy_flutter/test/unit_test/algorithm/levenshtein_test.dart deleted file mode 100644 index 2ccc3dad7a..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/algorithm/levenshtein_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/util/levenshtein.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('Levenshtein distance between identical strings', () { - final distance = levenshtein('abc', 'abc'); - expect(distance, 0); - }); - - test('Levenshtein distance between strings of different lengths', () { - final distance = levenshtein('kitten', 'sitting'); - expect(distance, 3); - }); - - test('Levenshtein distance between case-insensitive strings', () { - final distance = levenshtein('Hello', 'hello', caseSensitive: false); - expect(distance, 0); - }); - - test('Levenshtein distance between strings with substitutions', () { - final distance = levenshtein('kitten', 'smtten'); - expect(distance, 2); - }); - - test('Levenshtein distance between strings with deletions', () { - final distance = levenshtein('kitten', 'kiten'); - expect(distance, 1); - }); - - test('Levenshtein distance between strings with insertions', () { - final distance = levenshtein('kitten', 'kitxten'); - expect(distance, 1); - }); -} 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/editor_style_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/editor_style_test.dart deleted file mode 100644 index 4da21cc4a8..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/editor/editor_style_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockDocumentAppearanceCubit extends Mock - implements DocumentAppearanceCubit {} - -class MockBuildContext extends Mock implements BuildContext {} - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - group('EditorStyleCustomizer', () { - late EditorStyleCustomizer editorStyleCustomizer; - late MockBuildContext mockBuildContext; - - setUp(() { - mockBuildContext = MockBuildContext(); - editorStyleCustomizer = EditorStyleCustomizer( - context: mockBuildContext, - padding: EdgeInsets.zero, - ); - }); - - test('baseTextStyle should return the expected TextStyle', () { - const fontFamily = 'Roboto'; - final result = editorStyleCustomizer.baseTextStyle(fontFamily); - expect(result, isA()); - expect(result.fontFamily, 'Roboto_regular'); - }); - - test( - 'baseTextStyle should return the null TextStyle when an exception occurs', - () { - const garbage = 'Garbage'; - final result = editorStyleCustomizer.baseTextStyle(garbage); - expect(result, isA()); - expect( - result.fontFamily, - null, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart index d064e55ec7..6d39c5a0c1 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart @@ -1,6 +1,8 @@ import 'dart:convert'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -33,7 +35,7 @@ void main() { ); expect(result, r'$$E = MC^2$$'); }); - + // Changes test('code block', () { const text = ''' { @@ -41,13 +43,9 @@ void main() { "type":"page", "children":[ { - "type":"code", + "type":"code_block", "data":{ - "delta": [ - { - "insert": "Some Code" - } - ] + "code_block":"Some Code" } } ] @@ -65,7 +63,6 @@ void main() { ); expect(result, '```\nSome Code\n```'); }); - test('divider', () { const text = ''' { @@ -90,106 +87,5 @@ void main() { ); expect(result, '---\n'); }); - - test('callout', () { - const text = ''' -{ - "document":{ - "type":"page", - "children":[ - { - "type":"callout", - "data":{ - "icon": "😁", - "delta": [ - { - "insert": "Callout" - } - ] - } - } - ] - } -} -'''; - final document = Document.fromJson( - Map.from(json.decode(text)), - ); - final result = documentToMarkdown( - document, - customParsers: [ - const CalloutNodeParser(), - ], - ); - expect(result, '''> 😁 -> Callout - -'''); - }); - - test('toggle list', () { - const text = ''' -{ - "document":{ - "type":"page", - "children":[ - { - "type":"toggle_list", - "data":{ - "delta": [ - { - "insert": "Toggle list" - } - ] - } - } - ] - } -} -'''; - final document = Document.fromJson( - Map.from(json.decode(text)), - ); - final result = documentToMarkdown( - document, - customParsers: [ - const ToggleListNodeParser(), - ], - ); - expect(result, '- Toggle list\n'); - }); - - test('custom image', () { - const image = - 'https://images.unsplash.com/photo-1694984121999-36d30b67f391?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwzfHx8ZW58MHx8fHx8&auto=format&fit=crop&w=800&q=60'; - const text = ''' -{ - "document":{ - "type":"page", - "children":[ - { - "type":"image", - "data":{ - "url": "$image" - } - } - ] - } -} -'''; - final document = Document.fromJson( - Map.from(json.decode(text)), - ); - final result = documentToMarkdown( - document, - customParsers: [ - const CustomImageNodeParser(), - ], - ); - expect( - result, - '![]($image)\n', - ); - }); }); } 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 deleted file mode 100644 index 9e60c13ed7..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart +++ /dev/null @@ -1,396 +0,0 @@ -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:', () { - test('toBlockAction insert node with children operation', () { - final editorState = EditorState.blank(); - - final transaction = editorState.transaction; - transaction.insertNode( - [0], - paragraphNode( - children: [ - paragraphNode(text: '1', children: [paragraphNode(text: '1.1')]), - paragraphNode(text: '2'), - paragraphNode(text: '3', children: [paragraphNode(text: '3.1')]), - paragraphNode(text: '4'), - ], - ), - ); - - expect(transaction.operations.length, 1); - expect(transaction.operations[0] is InsertOperation, true); - - final actions = transaction.operations[0].toBlockAction(editorState, ''); - - expect(actions.length, 7); - for (final action in actions) { - expect(action.blockActionPB.action, BlockActionTypePB.Insert); - } - - expect( - actions[0].blockActionPB.payload.parentId, - editorState.document.root.id, - reason: '0 - parent id', - ); - expect( - actions[0].blockActionPB.payload.prevId, - '', - reason: '0 - prev id', - ); - expect( - actions[1].blockActionPB.payload.parentId, - actions[0].blockActionPB.payload.block.id, - reason: '1 - parent id', - ); - expect( - actions[1].blockActionPB.payload.prevId, - '', - reason: '1 - prev id', - ); - expect( - actions[2].blockActionPB.payload.parentId, - actions[1].blockActionPB.payload.block.id, - reason: '2 - parent id', - ); - expect( - actions[2].blockActionPB.payload.prevId, - '', - reason: '2 - prev id', - ); - expect( - actions[3].blockActionPB.payload.parentId, - actions[0].blockActionPB.payload.block.id, - reason: '3 - parent id', - ); - expect( - actions[3].blockActionPB.payload.prevId, - actions[1].blockActionPB.payload.block.id, - reason: '3 - prev id', - ); - expect( - actions[4].blockActionPB.payload.parentId, - actions[0].blockActionPB.payload.block.id, - reason: '4 - parent id', - ); - expect( - actions[4].blockActionPB.payload.prevId, - actions[3].blockActionPB.payload.block.id, - reason: '4 - prev id', - ); - expect( - actions[5].blockActionPB.payload.parentId, - actions[4].blockActionPB.payload.block.id, - reason: '5 - parent id', - ); - expect( - actions[5].blockActionPB.payload.prevId, - '', - reason: '5 - prev id', - ); - expect( - actions[6].blockActionPB.payload.parentId, - actions[0].blockActionPB.payload.block.id, - reason: '6 - parent id', - ); - expect( - actions[6].blockActionPB.payload.prevId, - actions[4].blockActionPB.payload.block.id, - reason: '6 - prev id', - ); - }); - - test('toBlockAction insert node before all children nodes', () { - final document = Document( - root: Node( - type: 'page', - children: [ - paragraphNode(children: [paragraphNode(text: '1')]), - ], - ), - ); - final editorState = EditorState(document: document); - - final transaction = editorState.transaction; - transaction.insertNodes([0, 0], [paragraphNode(), paragraphNode()]); - - expect(transaction.operations.length, 1); - expect(transaction.operations[0] is InsertOperation, true); - - final actions = transaction.operations[0].toBlockAction(editorState, ''); - - expect(actions.length, 2); - for (final action in actions) { - expect(action.blockActionPB.action, BlockActionTypePB.Insert); - } - - expect( - actions[0].blockActionPB.payload.parentId, - editorState.document.root.children.first.id, - reason: '0 - parent id', - ); - expect( - actions[0].blockActionPB.payload.prevId, - '', - reason: '0 - prev id', - ); - expect( - actions[1].blockActionPB.payload.parentId, - editorState.document.root.children.first.id, - reason: '1 - parent id', - ); - expect( - actions[1].blockActionPB.payload.prevId, - actions[0].blockActionPB.payload.block.id, - 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/select_option_split_text_input.dart b/frontend/appflowy_flutter/test/unit_test/select_option_split_text_input.dart index c13212df69..9ddad505dc 100644 --- a/frontend/appflowy_flutter/test/unit_test/select_option_split_text_input.dart +++ b/frontend/appflowy_flutter/test/unit_test/select_option_split_text_input.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -6,43 +6,41 @@ void main() { group('split input unit test', () { test('empty input', () { - var (submitted, remainder) = splitInput(' ', textSeparators); - expect(submitted, []); - expect(remainder, ''); + List result = splitInput(' ', textSeparators); + expect(result[0], []); + expect(result[1], ''); - (submitted, remainder) = splitInput(', , , ', textSeparators); - expect(submitted, []); - expect(remainder, ''); + result = splitInput(', , , ', textSeparators); + expect(result[0], []); + expect(result[1], ''); }); test('simple input', () { - var (submitted, remainder) = splitInput('exampleTag', textSeparators); - expect(submitted, []); - expect(remainder, 'exampleTag'); + List result = splitInput('exampleTag', textSeparators); + expect(result[0], []); + expect(result[1], 'exampleTag'); - (submitted, remainder) = - splitInput('tag with longer name', textSeparators); - expect(submitted, []); - expect(remainder, 'tag with longer name'); + result = splitInput('tag with longer name', textSeparators); + expect(result[0], []); + expect(result[1], 'tag with longer name'); - (submitted, remainder) = splitInput('trailing space ', textSeparators); - expect(submitted, []); - expect(remainder, 'trailing space '); + result = splitInput('trailing space ', textSeparators); + expect(result[0], []); + expect(result[1], 'trailing space '); }); test('input with commas', () { - var (submitted, remainder) = splitInput('a, b, c', textSeparators); - expect(submitted, ['a', 'b']); - expect(remainder, 'c'); + List result = splitInput('a, b, c', textSeparators); + expect(result[0], ['a', 'b']); + expect(result[1], 'c'); - (submitted, remainder) = splitInput('a, b, c, ', textSeparators); - expect(submitted, ['a', 'b', 'c']); - expect(remainder, ''); + result = splitInput('a, b, c, ', textSeparators); + expect(result[0], ['a', 'b', 'c']); + expect(result[1], ''); - (submitted, remainder) = - splitInput(',tag 1 ,2nd tag, third tag ', textSeparators); - expect(submitted, ['tag 1', '2nd tag']); - expect(remainder, 'third tag '); + result = splitInput(',tag 1 ,2nd tag, third tag ', textSeparators); + expect(result[0], ['tag 1', '2nd tag']); + expect(result[1], 'third tag '); }); }); } diff --git a/frontend/appflowy_flutter/test/unit_test/settings/shortcuts/settings_shortcut_service_test.dart b/frontend/appflowy_flutter/test/unit_test/settings/shortcuts/settings_shortcut_service_test.dart deleted file mode 100644 index 3aea2755f4..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/settings/shortcuts/settings_shortcut_service_test.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:convert'; -import 'dart:io' show File; - -import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; -import 'package:appflowy/workspace/application/settings/shortcuts/shortcuts_model.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: depend_on_referenced_packages -import 'package:file/memory.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - late SettingsShortcutService service; - late File mockFile; - String shortcutsJson = ''; - - setUp(() async { - final MemoryFileSystem fileSystem = MemoryFileSystem.test(); - mockFile = await fileSystem.file("shortcuts.json").create(recursive: true); - service = SettingsShortcutService(file: mockFile); - shortcutsJson = """{ - "commandShortcuts":[ - { - "key":"move the cursor upward", - "command":"alt+arrow up" - }, - { - "key":"move the cursor backward one character", - "command":"alt+arrow left" - }, - { - "key":"move the cursor downward", - "command":"alt+arrow down" - } - ] -}"""; - }); - - group("Settings Shortcut Service", () { - test( - "returns default standard shortcuts if file is empty", - () async { - expect(await service.getCustomizeShortcuts(), []); - }, - ); - - test('returns updated shortcut event list from json', () { - final commandShortcuts = service.getShortcutsFromJson(shortcutsJson); - - final cursorUpShortcut = commandShortcuts - .firstWhere((el) => el.key == "move the cursor upward"); - - final cursorDownShortcut = commandShortcuts - .firstWhere((el) => el.key == "move the cursor downward"); - - expect( - commandShortcuts.length, - 3, - ); - expect(cursorUpShortcut.command, "alt+arrow up"); - expect(cursorDownShortcut.command, "alt+arrow down"); - }); - - test( - "saveAllShortcuts saves shortcuts", - () async { - //updating one of standard command shortcut events. - final currentCommandShortcuts = standardCommandShortcutEvents; - const kKey = "scroll one page down"; - const oldCommand = "page down"; - const newCommand = "alt+page down"; - final commandShortcutEvent = currentCommandShortcuts - .firstWhere((element) => element.key == kKey); - - expect(commandShortcutEvent.command, oldCommand); - - //updating the command. - commandShortcutEvent.updateCommand( - command: newCommand, - ); - - //saving the updated shortcuts - await service.saveAllShortcuts(currentCommandShortcuts); - - //reading from the mock file the saved shortcut list. - final savedDataInFile = await mockFile.readAsString(); - - //Check if the lists where properly converted to JSON and saved. - final shortcuts = EditorShortcuts( - commandShortcuts: - currentCommandShortcuts.toCommandShortcutModelList(), - ); - - expect(jsonEncode(shortcuts.toJson()), savedDataInFile); - - //now checking if the modified command of "move the cursor upward" is "arrow up" - final newCommandShortcuts = - service.getShortcutsFromJson(savedDataInFile); - - final updatedCommandEvent = - newCommandShortcuts.firstWhere((el) => el.key == kKey); - - expect(updatedCommandEvent.command, newCommand); - }, - ); - - test('load shortcuts from file', () async { - //updating one of standard command shortcut event. - const kKey = "scroll one page up"; - const oldCommand = "page up"; - const newCommand = "alt+page up"; - final currentCommandShortcuts = standardCommandShortcutEvents; - final commandShortcutEvent = - currentCommandShortcuts.firstWhere((element) => element.key == kKey); - - expect(commandShortcutEvent.command, oldCommand); - - //updating the command. - commandShortcutEvent.updateCommand(command: newCommand); - - //saving the updated shortcuts - await service.saveAllShortcuts(currentCommandShortcuts); - - //now directly fetching the shortcuts from loadShortcuts - final commandShortcuts = await service.getCustomizeShortcuts(); - expect( - commandShortcuts, - currentCommandShortcuts.toCommandShortcutModelList(), - ); - - final updatedCommandEvent = - commandShortcuts.firstWhere((el) => el.key == kKey); - - expect(updatedCommandEvent.command, newCommand); - }); - - test('updateCommandShortcuts works properly', () async { - //updating one of standard command shortcut event. - const kKey = "move the cursor backward one character"; - const oldCommand = "arrow left"; - const newCommand = "alt+arrow left"; - final currentCommandShortcuts = standardCommandShortcutEvents; - - //check if the current shortcut event's key is set to old command. - final currentCommandEvent = - currentCommandShortcuts.firstWhere((el) => el.key == kKey); - - expect(currentCommandEvent.command, oldCommand); - - final commandShortcutModelList = - EditorShortcuts.fromJson(jsonDecode(shortcutsJson)).commandShortcuts; - - //now calling the updateCommandShortcuts method - await service.updateCommandShortcuts( - currentCommandShortcuts, - commandShortcutModelList, - ); - - //check if the shortcut event's key is updated. - final updatedCommandEvent = - currentCommandShortcuts.firstWhere((el) => el.key == kKey); - - expect(updatedCommandEvent.command, newCommand); - }); - }); -} - -extension on List { - List toCommandShortcutModelList() => - map((e) => CommandShortcutModel.fromCommandEvent(e)).toList(); -} 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/theme/theme_test.dart b/frontend/appflowy_flutter/test/unit_test/theme/theme_test.dart deleted file mode 100644 index 5563c93c15..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/theme/theme_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flowy_infra/colorscheme/colorscheme.dart'; -import 'package:flowy_infra/plugins/service/location_service.dart'; -import 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart'; -import 'package:flowy_infra/plugins/service/plugin_service.dart'; -import 'package:flowy_infra/theme.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class MockPluginService implements FlowyPluginService { - @override - Future addPlugin(FlowyDynamicPlugin plugin) => - throw UnimplementedError(); - - @override - Future lookup({required String name}) => - throw UnimplementedError(); - - @override - Future get plugins async => const Iterable.empty(); - - @override - void setLocation(PluginLocationService locationService) => - throw UnimplementedError(); -} - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - group('AppTheme', () { - test('fallback theme', () { - const theme = AppTheme.fallback; - - expect(theme.builtIn, true); - expect(theme.themeName, BuiltInTheme.defaultTheme); - expect(theme.lightTheme, isA()); - expect(theme.darkTheme, isA()); - }); - - test('built-in themes', () { - final themes = AppTheme.builtins; - - expect(themes, isNotEmpty); - for (final theme in themes) { - expect(theme.builtIn, true); - expect( - theme.themeName, - anyOf([ - BuiltInTheme.defaultTheme, - BuiltInTheme.dandelion, - BuiltInTheme.lavender, - BuiltInTheme.lemonade, - ]), - ); - expect(theme.lightTheme, isA()); - expect(theme.darkTheme, isA()); - } - }); - - test('fromName returns existing theme', () async { - final theme = await AppTheme.fromName( - BuiltInTheme.defaultTheme, - pluginService: MockPluginService(), - ); - - expect(theme, isNotNull); - expect(theme.builtIn, true); - expect(theme.themeName, BuiltInTheme.defaultTheme); - expect(theme.lightTheme, isA()); - expect(theme.darkTheme, isA()); - }); - - test('fromName throws error for non-existent theme', () async { - expect( - () async => AppTheme.fromName( - 'bogus', - pluginService: MockPluginService(), - ), - throwsArgumentError, - ); - }); - }); -} 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/unit_test/util/time.dart b/frontend/appflowy_flutter/test/unit_test/util/time.dart deleted file mode 100644 index ca4f2b8230..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/util/time.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:appflowy/util/time.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('parseTime should parse time string to minutes', () { - expect(parseTime('10'), 10); - expect(parseTime('70m'), 70); - expect(parseTime('4h 20m'), 260); - expect(parseTime('1h 80m'), 140); - expect(parseTime('asffsa2h3m'), null); - expect(parseTime('2h3m'), null); - expect(parseTime('blah'), null); - expect(parseTime('10a'), null); - expect(parseTime('2h'), 120); - }); - - test('formatTime should format time minutes to formatted string', () { - expect(formatTime(5), "5m"); - expect(formatTime(75), "1h 15m"); - expect(formatTime(120), "2h"); - expect(formatTime(-50), ""); - expect(formatTime(0), "0m"); - }); -} diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 3bb774411b..137613cddd 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -3,20 +3,31 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.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_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flowy_infra/uuid.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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:appflowy/main.dart'; import 'package:shared_preferences/shared_preferences.dart'; +class AppFlowyIntegrateTest { + static Future ensureInitialized() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + main(); + return AppFlowyIntegrateTest(); + } +} + class AppFlowyUnitTest { late UserProfilePB userProfile; late UserBackendService userService; late WorkspaceService workspaceService; - late WorkspacePB workspace; + late List workspaces; static Future ensureInitialized() async { TestWidgetsFlutterBinding.ensureInitialized(); @@ -24,8 +35,8 @@ class AppFlowyUnitTest { _pathProviderInitialized(); await FlowyRunner.run( - AppFlowyApplicationUnitTest(), - IntegrationMode.unitTest, + FlowyTestApp(), + IntegrationMode.test, ); final test = AppFlowyUnitTest(); @@ -47,21 +58,22 @@ class AppFlowyUnitTest { email: userEmail, ); result.fold( + (error) { + assert(false, 'Error: $error'); + }, (user) { userProfile = user; userService = UserBackendService(userId: userProfile.id); }, - (error) { - assert(false, 'Error: $error'); - }, ); } - WorkspacePB get currentWorkspace => workspace; + WorkspacePB get currentWorkspace => workspaces[0]; + Future _loadWorkspace() async { - final result = await UserBackendService.getCurrentWorkspace(); + final result = await userService.getWorkspaces(); result.fold( - (value) => workspace = value, + (value) => workspaces = value, (error) { throw Exception(error); }, @@ -69,19 +81,22 @@ class AppFlowyUnitTest { } Future _initialServices() async { - workspaceService = WorkspaceService( - workspaceId: currentWorkspace.id, - userId: userProfile.id, + workspaceService = WorkspaceService(workspaceId: currentWorkspace.id); + } + + Future createTestApp() async { + final result = await workspaceService.createApp(name: "Test App"); + return result.fold( + (app) => app, + (error) => throw Exception(error), ); } - Future createWorkspace() async { - final result = await workspaceService.createView( - name: "Test App", - viewSection: ViewSectionPB.Public, - ); + Future> loadApps() async { + final result = await workspaceService.getViews(); + return result.fold( - (app) => app, + (apps) => apps, (error) => throw Exception(error), ); } @@ -96,10 +111,10 @@ void _pathProviderInitialized() { }); } -class AppFlowyApplicationUnitTest implements EntryPoint { +class FlowyTestApp implements EntryPoint { @override Widget create(LaunchConfiguration config) { - return const SizedBox.shrink(); + return Container(); } } 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 4458d588cc..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.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 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 deleted file mode 100644 index 4d954d1724..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/user/application/user_settings_service.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../util.dart'; - -class MockAppearanceSettingsBloc - extends MockBloc - implements AppearanceSettingsCubit {} - -class MockDocumentAppearanceCubit extends Mock - implements DocumentAppearanceCubit {} - -class MockDocumentAppearance extends Mock implements DocumentAppearance {} - -void main() { - late AppearanceSettingsPB appearanceSettings; - late DateTimeSettingsPB dateTimeSettings; - - setUp(() async { - await AppFlowyUnitTest.ensureInitialized(); - appearanceSettings = - await UserSettingsBackendService().getAppearanceSetting(); - dateTimeSettings = await UserSettingsBackendService().getDateTimeSettings(); - registerFallbackValue(AppFlowyTextDirection.ltr); - }); - - testWidgets('TextDirectionSelect update default text direction setting', - (WidgetTester tester) async { - final appearanceSettingsState = AppearanceSettingsState.initial( - AppTheme.fallback, - appearanceSettings.themeMode, - appearanceSettings.font, - appearanceSettings.layoutDirection, - appearanceSettings.textDirection, - appearanceSettings.enableRtlToolbarItems, - appearanceSettings.locale, - appearanceSettings.isMenuCollapsed, - appearanceSettings.menuOffset, - dateTimeSettings.dateFormat, - dateTimeSettings.timeFormat, - dateTimeSettings.timezoneId, - appearanceSettings.documentSetting.cursorColor.isEmpty - ? null - : Color( - int.parse(appearanceSettings.documentSetting.cursorColor), - ), - appearanceSettings.documentSetting.selectionColor.isEmpty - ? null - : Color( - int.parse( - appearanceSettings.documentSetting.selectionColor, - ), - ), - 1.0, - ); - final mockAppearanceSettingsBloc = MockAppearanceSettingsBloc(); - when(() => mockAppearanceSettingsBloc.state).thenReturn( - appearanceSettingsState, - ); - - final mockDocumentAppearanceCubit = MockDocumentAppearanceCubit(); - when(() => mockDocumentAppearanceCubit.stream).thenAnswer( - (_) => Stream.fromIterable([MockDocumentAppearance()]), - ); - - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value( - value: mockAppearanceSettingsBloc, - ), - BlocProvider.value( - value: mockDocumentAppearanceCubit, - ), - ], - child: MaterialApp( - theme: appearanceSettingsState.lightTheme, - home: MultiBlocProvider( - providers: [ - BlocProvider.value( - value: mockAppearanceSettingsBloc, - ), - BlocProvider.value( - value: mockDocumentAppearanceCubit, - ), - ], - child: const Scaffold( - body: TextDirectionSelect(), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect( - find.text( - LocaleKeys.settings_workspacePage_textDirection_leftToRight.tr(), - ), - findsOne, - ); - expect( - find.text( - LocaleKeys.settings_workspacePage_textDirection_rightToLeft.tr(), - ), - findsOne, - ); - expect( - find.text( - LocaleKeys.settings_workspacePage_textDirection_auto.tr(), - ), - findsOne, - ); - - final radioSelectFinder = - find.byType(SettingsRadioSelect); - expect(radioSelectFinder, findsOne); - - when( - () => mockAppearanceSettingsBloc.setTextDirection( - any(), - ), - ).thenAnswer((_) async => {}); - when( - () => mockDocumentAppearanceCubit.syncDefaultTextDirection( - any(), - ), - ).thenAnswer((_) async {}); - - final radioSelect = tester.widget(radioSelectFinder) - as SettingsRadioSelect; - final rtlSelect = radioSelect.items - .firstWhere((select) => select.value == AppFlowyTextDirection.rtl); - radioSelect.onChanged(rtlSelect); - - verify( - () => mockAppearanceSettingsBloc.setTextDirection( - any(), - ), - ).called(1); - verify( - () => mockDocumentAppearanceCubit.syncDefaultTextDirection( - any(), - ), - ).called(1); - }); -} diff --git a/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart b/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart index b1e2e4ccea..c1fb2eb6d9 100644 --- a/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart @@ -1,9 +1,10 @@ import 'dart:collection'; -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:textfield_tags/textfield_tags.dart'; import '../bloc_test/grid_test/util.dart'; @@ -17,13 +18,12 @@ void main() { String remainder = ''; List select = []; - final textController = TextEditingController(); - final textField = SelectOptionTextField( options: const [], selectedOptionMap: LinkedHashMap(), distanceToText: 0.0, - onSubmitted: () => submit = textController.text, + tagController: TextfieldTagsController(), + onSubmitted: (text) => submit = text, onPaste: (options, remaining) { remainder = remaining; select = options; @@ -31,8 +31,7 @@ void main() { onRemove: (_) {}, newText: (text) => remainder = text, textSeparators: const [','], - textController: textController, - focusNode: FocusNode(), + textController: TextEditingController(), ); testWidgets('SelectOptionTextField callback outputs', @@ -60,6 +59,11 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); expect(submit, 'an option'); + submit = ''; + await tester.enterText(find.byType(TextField), ' '); + await tester.testTextInput.receiveAction(TextInputAction.done); + expect(submit, ''); + // test inputs containing commas await tester.enterText(find.byType(TextField), 'a a, bbbb , c'); expect(remainder, 'c'); 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/test/widget_test/theme_font_family_setting_test.dart b/frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart deleted file mode 100644 index 19c36a8b59..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -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/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.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_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockAppearanceSettingsCubit extends Mock - implements AppearanceSettingsCubit {} - -class MockDocumentAppearanceCubit extends Mock - implements DocumentAppearanceCubit {} - -class MockAppearanceSettingsState extends Mock - implements AppearanceSettingsState {} - -class MockDocumentAppearance extends Mock implements DocumentAppearance {} - -void main() { - late MockAppearanceSettingsCubit appearanceSettingsCubit; - late MockDocumentAppearanceCubit documentAppearanceCubit; - - setUp(() { - appearanceSettingsCubit = MockAppearanceSettingsCubit(); - when(() => appearanceSettingsCubit.stream).thenAnswer( - (_) => Stream.fromIterable([MockAppearanceSettingsState()]), - ); - documentAppearanceCubit = MockDocumentAppearanceCubit(); - when(() => documentAppearanceCubit.stream).thenAnswer( - (_) => Stream.fromIterable([MockDocumentAppearance()]), - ); - }); - - testWidgets('ThemeFontFamilySetting updates font family on selection', - (WidgetTester tester) async { - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value( - value: appearanceSettingsCubit, - ), - BlocProvider.value( - value: documentAppearanceCubit, - ), - ], - child: MaterialApp( - home: MultiBlocProvider( - providers: [ - BlocProvider.value( - value: appearanceSettingsCubit, - ), - BlocProvider.value( - value: documentAppearanceCubit, - ), - ], - child: const Scaffold( - body: ThemeFontFamilySetting( - currentFontFamily: defaultFontFamily, - ), - ), - ), - ), - ), - ); - - final popover = find.byType(AppFlowyPopover); - await tester.tap(popover); - await tester.pumpAndSettle(); - - // Verify the initial font family - expect( - find.text(LocaleKeys.settings_appearance_fontFamily_defaultFont.tr()), - findsAtLeastNWidgets(1), - ); - when(() => appearanceSettingsCubit.setFontFamily(any())) - .thenAnswer((_) async {}); - verifyNever(() => appearanceSettingsCubit.setFontFamily(any())); - when(() => documentAppearanceCubit.syncFontFamily(any())) - .thenAnswer((_) async {}); - verifyNever(() => documentAppearanceCubit.syncFontFamily(any())); - - // Tap on a different font family - final abel = find.textContaining('Abel'); - await tester.tap(abel); - await tester.pumpAndSettle(); - - // Verify that the font family is updated - verify(() => appearanceSettingsCubit.setFontFamily(any())) - .called(1); - verify(() => documentAppearanceCubit.syncFontFamily(any())) - .called(1); - }); -} diff --git a/frontend/appflowy_flutter/windows/flutter/CMakeLists.txt b/frontend/appflowy_flutter/windows/flutter/CMakeLists.txt index c1a3afe639..8ec917b8c3 100644 --- a/frontend/appflowy_flutter/windows/flutter/CMakeLists.txt +++ b/frontend/appflowy_flutter/windows/flutter/CMakeLists.txt @@ -9,11 +9,6 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -97,7 +92,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ + windows-x64 $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/frontend/appflowy_flutter/windows/runner/Runner.rc b/frontend/appflowy_flutter/windows/runner/Runner.rc index 3477dab755..0639f41ec5 100644 --- a/frontend/appflowy_flutter/windows/runner/Runner.rc +++ b/frontend/appflowy_flutter/windows/runner/Runner.rc @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "AppFlowy" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "AppFlowy" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 io.appflowy. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 io.appflowy. All rights reserved." "\0" VALUE "OriginalFilename", "AppFlowy.exe" "\0" VALUE "ProductName", "AppFlowy" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" @@ -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..b43b9095ea 100644 --- a/frontend/appflowy_flutter/windows/runner/flutter_window.cpp +++ b/frontend/appflowy_flutter/windows/runner/flutter_window.cpp @@ -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..2f7c10b343 100644 --- a/frontend/appflowy_flutter/windows/runner/main.cpp +++ b/frontend/appflowy_flutter/windows/runner/main.cpp @@ -5,32 +5,13 @@ #include "flutter_window.h" #include "utils.h" -#include -auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); - int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"AppFlowyMutex"); - HWND handle = FindWindowA(NULL, "AppFlowy"); - - if (GetLastError() == ERROR_ALREADY_EXISTS) { - flutter::DartProject project(L"data"); - std::vector command_line_arguments = GetCommandLineArguments(); - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - FlutterWindow window(project); - if (window.SendAppLinkToInstance(L"AppFlowy")) { - return false; - } - - WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)}; - GetWindowPlacement(handle, &place); - ShowWindow(handle, SW_NORMAL); - return 0; - } - + _In_ wchar_t *command_line, _In_ int show_command) +{ // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { CreateAndAttachConsole(); } @@ -40,28 +21,27 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, flutter::DartProject project(L"data"); - std::vector command_line_arguments = GetCommandLineArguments(); + std::vector command_line_arguments = + GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 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; - while (::GetMessage(&msg, nullptr, 0, 0)) { + while (::GetMessage(&msg, nullptr, 0, 0)) + { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); - ReleaseMutex(hMutexInstance); return EXIT_SUCCESS; } diff --git a/frontend/appflowy_flutter/windows/runner/win32_window.cpp b/frontend/appflowy_flutter/windows/runner/win32_window.cpp index 2f78196d35..c10f08dc7d 100644 --- a/frontend/appflowy_flutter/windows/runner/win32_window.cpp +++ b/frontend/appflowy_flutter/windows/runner/win32_window.cpp @@ -1,33 +1,13 @@ #include "win32_window.h" -#include #include #include "resource.h" -#include "app_links/app_links_plugin_c_api.h" - 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"; -/// 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"; - // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; @@ -51,8 +31,8 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) { GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); } - FreeLibrary(user32_module); } } // namespace @@ -62,7 +42,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registrar instance. + // Returns the singleton registar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); @@ -122,16 +102,11 @@ Win32Window::~Win32Window() { Destroy(); } -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { Destroy(); - if (SendAppLinkToInstance(title)) - { - return false; - } - const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); @@ -142,7 +117,7 @@ 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); @@ -151,15 +126,9 @@ bool Win32Window::Create(const std::wstring& title, 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, @@ -219,10 +188,6 @@ Win32Window::MessageHandler(HWND hwnd, SetFocus(child_content_); } return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); @@ -278,55 +243,3 @@ bool Win32Window::OnCreate() { 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 - HWND hwnd = ::FindWindow(kWindowClassName, title.c_str()); - - if (hwnd) - { - // Dispatch new link to current window - SendAppLink(hwnd); - - // (Optional) Restore our window to front in same state - WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)}; - GetWindowPlacement(hwnd, &place); - - switch (place.showCmd) - { - case SW_SHOWMAXIMIZED: - ShowWindow(hwnd, SW_SHOWMAXIMIZED); - break; - case SW_SHOWMINIMIZED: - ShowWindow(hwnd, SW_RESTORE); - break; - default: - ShowWindow(hwnd, SW_NORMAL); - break; - } - - SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); - SetForegroundWindow(hwnd); - - // Window has been found, don't create another one. - return true; - } - - return false; -} \ No newline at end of file diff --git a/frontend/appflowy_flutter/windows/runner/win32_window.h b/frontend/appflowy_flutter/windows/runner/win32_window.h index fae0d8a741..17ba431125 100644 --- a/frontend/appflowy_flutter/windows/runner/win32_window.h +++ b/frontend/appflowy_flutter/windows/runner/win32_window.h @@ -28,16 +28,15 @@ 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); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); + // 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); // Release OS resources associated with window. void Destroy(); @@ -55,10 +54,6 @@ 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: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that @@ -81,7 +76,7 @@ class Win32Window { // 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, @@ -91,9 +86,6 @@ class Win32Window { // 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); - bool quit_on_close_ = false; // window handle for top level window. 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..6bb08b783d --- /dev/null +++ b/frontend/appflowy_tauri/.eslintrc.cjs @@ -0,0 +1,72 @@ +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, + }, + plugins: ['@typescript-eslint', "react-hooks"], + rules: { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-empty-interface': 'warn', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/prefer-for-of': 'warn', + '@typescript-eslint/triple-slash-reference': 'error', + '@typescript-eslint/unified-signatures': 'warn', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'warn', + '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-redeclare': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unsafe-finally': 'error', + 'no-unused-labels': 'error', + 'no-var': 'warn', + 'no-void': 'off', + 'prefer-const': 'warn', + 'prefer-spread': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + } + ], + 'padding-line-between-statements': [ + "warn", + { 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'], +}; diff --git a/frontend/appflowy_tauri/.gitignore b/frontend/appflowy_tauri/.gitignore new file mode 100644 index 0000000000..0ae4944041 --- /dev/null +++ b/frontend/appflowy_tauri/.gitignore @@ -0,0 +1,27 @@ +# 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/ \ 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..194012b116 --- /dev/null +++ b/frontend/appflowy_tauri/index.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + React + TS + + + +
+ + + diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json new file mode 100644 index 0000000000..bfe25ad27f --- /dev/null +++ b/frontend/appflowy_tauri/package.json @@ -0,0 +1,86 @@ +{ + "name": "appflowy_tauri", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "format": "prettier --write .", + "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", + "test:errors": "tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .", + "test:prettier": "yarn prettier --list-different src", + "tauri:clean": "cargo make --cwd .. tauri_clean", + "tauri:dev": "tauri dev" + }, + "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", + "@reduxjs/toolkit": "^1.9.2", + "@slate-yjs/core": "^1.0.0", + "@tanstack/react-virtual": "3.0.0-beta.54", + "@tauri-apps/api": "^1.2.0", + "dayjs": "^1.11.7", + "emoji-mart": "^5.5.2", + "events": "^3.3.0", + "google-protobuf": "^3.21.2", + "i18next": "^22.4.10", + "i18next-browser-languagedetector": "^7.0.1", + "is-hotkey": "^0.2.0", + "jest": "^29.5.0", + "nanoid": "^4.0.0", + "prismjs": "^1.29.0", + "protoc-gen-ts": "^0.8.5", + "quill": "^1.3.7", + "quill-delta": "^5.1.0", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-calendar": "^4.1.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", + "react-i18next": "^12.2.0", + "react-redux": "^8.0.5", + "react-router-dom": "^6.8.0", + "react18-input-otp": "^1.1.2", + "redux": "^4.2.1", + "rxjs": "^7.8.0", + "slate": "^0.94.1", + "slate-react": "^0.94.2", + "ts-results": "^3.3.0", + "utf8": "^3.0.0", + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.51" + }, + "devDependencies": { + "@tauri-apps/cli": "^1.2.2", + "@types/google-protobuf": "^3.15.6", + "@types/is-hotkey": "^0.1.7", + "@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-dom": "^18.0.6", + "@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", + "eslint": "^8.34.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "postcss": "^8.4.21", + "prettier": "2.8.4", + "prettier-plugin-tailwindcss": "^0.2.2", + "tailwindcss": "^3.2.7", + "typescript": "^4.6.4", + "uuid": "^9.0.0", + "vite": "^4.0.0" + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml new file mode 100644 index 0000000000..d725aacd21 --- /dev/null +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -0,0 +1,5327 @@ +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) + '@reduxjs/toolkit': + specifier: ^1.9.2 + version: 1.9.5(react-redux@8.0.5)(react@18.2.0) + '@slate-yjs/core': + specifier: ^1.0.0 + version: 1.0.0(slate@0.94.1)(yjs@13.6.1) + '@tanstack/react-virtual': + specifier: 3.0.0-beta.54 + version: 3.0.0-beta.54(react@18.2.0) + '@tauri-apps/api': + specifier: ^1.2.0 + version: 1.3.0 + dayjs: + specifier: ^1.11.7 + version: 1.11.7 + emoji-mart: + specifier: ^5.5.2 + version: 5.5.2 + events: + specifier: ^3.3.0 + version: 3.3.0 + google-protobuf: + specifier: ^3.21.2 + version: 3.21.2 + i18next: + specifier: ^22.4.10 + version: 22.4.15 + i18next-browser-languagedetector: + specifier: ^7.0.1 + version: 7.0.1 + is-hotkey: + specifier: ^0.2.0 + version: 0.2.0 + jest: + specifier: ^29.5.0 + version: 29.5.0(@types/node@18.16.9) + nanoid: + specifier: ^4.0.0 + version: 4.0.2 + prismjs: + specifier: ^1.29.0 + version: 1.29.0 + protoc-gen-ts: + specifier: ^0.8.5 + version: 0.8.6(google-protobuf@3.21.2)(typescript@4.9.5) + 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-calendar: + specifier: ^4.1.0 + version: 4.2.1(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-i18next: + specifier: ^12.2.0 + version: 12.2.2(i18next@22.4.15)(react-dom@18.2.0)(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) + 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 + slate: + specifier: ^0.94.1 + version: 0.94.1 + slate-react: + specifier: ^0.94.2 + version: 0.94.2(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1) + ts-results: + specifier: ^3.3.0 + version: 3.3.0 + utf8: + specifier: ^3.0.0 + version: 3.0.0 + y-indexeddb: + specifier: ^9.0.9 + version: 9.0.11(yjs@13.6.1) + yjs: + specifier: ^13.5.51 + version: 13.6.1 + +devDependencies: + '@tauri-apps/cli': + specifier: ^1.2.2 + version: 1.3.1 + '@types/google-protobuf': + specifier: ^3.15.6 + version: 3.15.6 + '@types/is-hotkey': + specifier: ^0.1.7 + version: 0.1.7 + '@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-dom': + specifier: ^18.0.6 + version: 18.2.4 + '@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) + 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) + 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) + tailwindcss: + specifier: ^3.2.7 + version: 3.3.2 + 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) + +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/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.21.5 + '@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/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-function-name@7.21.0: + resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/types': 7.21.5 + + /@babel/helper-hoist-variables@7.18.6: + resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 + + /@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.21.5 + '@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-string-parser@7.21.5: + resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} + 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-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.21.5 + '@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/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/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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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.21.5: + resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: false + + /@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/traverse@7.21.5: + resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.5 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 + 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 + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: false + + /@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 + + /@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 + + /@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 + dev: false + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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.5.0 + slash: 3.0.0 + dev: false + + /@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 + dev: false + + /@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 + dev: false + + /@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 + + /@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/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.12.3(@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.12.3(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-o1e7Z1Bp27n4x2iUHhegV4/Jp6H3T6iBKHJdLivS5GbwsuAE/5l4SnZ+7+K+e5u9TuhwcAKZLkjvqzkDe8zqfA==} + 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.21.5 + '@mui/utils': 5.12.3(react@18.2.0) + '@types/react': 18.2.6 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/styled-engine@5.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-AhZtiRyT8Bjr7fufxE/mLS+QJ3LxwX1kghIcM2B2dvJzSSg9rnIuXDXM959QfUVIM3C8U4x3mgVoPFMQJvc4/g==} + 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.21.5 + '@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.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-JB/6sypHqeJCqwldWeQ1MKkijH829EcZAKKizxbU2MJdxGG5KSwZvTBa5D9qiJUA1hJFYYupjiuy9ZdJt6rV6w==} + 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.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/private-theming': 5.12.3(@types/react@18.2.6)(react@18.2.0) + '@mui/styled-engine': 5.12.3(@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.12.3(react@18.2.0) + '@types/react': 18.2.6 + clsx: 1.2.1 + 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/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 + + /@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 + + /@reduxjs/toolkit@1.9.5(react-redux@8.0.5)(react@18.2.0): + resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 9.0.21 + 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: 4.2.1 + redux-thunk: 2.4.2(redux@4.2.1) + reselect: 4.1.8 + dev: false + + /@remix-run/router@1.6.1: + resolution: {integrity: sha512-YUkWj+xs0oOzBe74OgErsuR3wVn+efrFhXBWrit50kOiED+pvQe2r6MWY0iJMQU/mSVKxvNzL4ZaYvjdX+G7ZA==} + engines: {node: '>=14'} + dev: false + + /@sinclair/typebox@0.25.24: + resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + dev: false + + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: false + + /@sinonjs/fake-timers@10.1.0: + resolution: {integrity: sha512-w1qd368vtrwttm1PRJWPW1QHlbmHrVDGs1eBH/jZvRPUFS4MNXV9Q33EQdjOdeAxZ7O8+3wM7zxztm2nfUSyKw==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: false + + /@slate-yjs/core@1.0.0(slate@0.94.1)(yjs@13.6.1): + resolution: {integrity: sha512-G83+qvXtsMTP3kWu216GjhyeHlvKHX5kWaPf2JiG2uF5/YShUqjAVjDr/htKoKJsOl+IqK679lvLKeBYh7SYZQ==} + peerDependencies: + slate: '>=0.70.0' + yjs: ^13.5.29 + dependencies: + slate: 0.94.1 + y-protocols: 1.0.5 + yjs: 13.6.1 + dev: false + + /@tanstack/react-virtual@3.0.0-beta.54(react@18.2.0): + resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.0.0-beta.54 + react: 18.2.0 + dev: false + + /@tanstack/virtual-core@3.0.0-beta.54: + resolution: {integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==} + dev: false + + /@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.3.1: + resolution: {integrity: sha512-QlepYVPgOgspcwA/u4kGG4ZUijlXfdRtno00zEy+LxinN/IRXtk+6ErVtsmoLi1ZC9WbuMwzAcsRvqsD+RtNAg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-darwin-x64@1.3.1: + resolution: {integrity: sha512-fKcAUPVFO3jfDKXCSDGY0MhZFF/wDtx3rgFnogWYu4knk38o9RaqRkvMvqJhLYPuWaEM5h6/z1dRrr9KKCbrVg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm-gnueabihf@1.3.1: + resolution: {integrity: sha512-+4H0dv8ltJHYu/Ma1h9ixUPUWka9EjaYa8nJfiMsdCI4LJLNE6cPveE7RmhZ59v9GW1XB108/k083JUC/OtGvA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-gnu@1.3.1: + resolution: {integrity: sha512-Pj3odVO1JAxLjYmoXKxcrpj/tPxcA8UP8N06finhNtBtBaxAjrjjxKjO4968KB0BUH7AASIss9EL4Tr0FGnDuw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-musl@1.3.1: + resolution: {integrity: sha512-tA0JdDLPFaj42UDIVcF2t8V0tSha40rppcmAR/MfQpTCxih6399iMjwihz9kZE1n4b5O4KTq9GliYo50a8zYlQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-gnu@1.3.1: + resolution: {integrity: sha512-FDU+Mnvk6NLkqQimcNojdKpMN4Y3W51+SQl+NqG9AFCWprCcSg62yRb84751ujZuf2MGT8HQOfmd0i77F4Q3tQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-musl@1.3.1: + resolution: {integrity: sha512-MpO3akXFmK8lZYEbyQRDfhdxz1JkTBhonVuz5rRqxwA7gnGWHa1aF1+/2zsy7ahjB2tQ9x8DDFDMdVE20o9HrA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-ia32-msvc@1.3.1: + resolution: {integrity: sha512-9Boeo3K5sOrSBAZBuYyGkpV2RfnGQz3ZhGJt4hE6P+HxRd62lS6+qDKAiw1GmkZ0l1drc2INWrNeT50gwOKwIQ==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-x64-msvc@1.3.1: + resolution: {integrity: sha512-wMrTo91hUu5CdpbElrOmcZEoJR4aooTG+fbtcc87SMyPGQy1Ux62b+ZdwLvL1sVTxnIm//7v6QLRIWGiUjCPwA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli@1.3.1: + resolution: {integrity: sha512-o4I0JujdITsVRm3/0spfJX7FcKYrYV1DXJqzlWIn6IY25/RltjU6qbC1TPgVww3RsRX63jyVUTcWpj5wwFl+EQ==} + engines: {node: '>= 10'} + hasBin: true + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 1.3.1 + '@tauri-apps/cli-darwin-x64': 1.3.1 + '@tauri-apps/cli-linux-arm-gnueabihf': 1.3.1 + '@tauri-apps/cli-linux-arm64-gnu': 1.3.1 + '@tauri-apps/cli-linux-arm64-musl': 1.3.1 + '@tauri-apps/cli-linux-x64-gnu': 1.3.1 + '@tauri-apps/cli-linux-x64-musl': 1.3.1 + '@tauri-apps/cli-win32-ia32-msvc': 1.3.1 + '@tauri-apps/cli-win32-x64-msvc': 1.3.1 + 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 + dev: false + + /@types/babel__generator@7.6.4: + resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} + dependencies: + '@babel/types': 7.21.5 + dev: false + + /@types/babel__template@7.4.1: + resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} + dependencies: + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 + dev: false + + /@types/babel__traverse@7.18.5: + resolution: {integrity: sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q==} + dependencies: + '@babel/types': 7.21.5 + dev: false + + /@types/google-protobuf@3.15.6: + resolution: {integrity: sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==} + dev: true + + /@types/graceful-fs@4.1.6: + resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} + dependencies: + '@types/node': 18.16.9 + dev: false + + /@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.7: + resolution: {integrity: sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ==} + + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: false + + /@types/istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + dev: false + + /@types/istanbul-reports@3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + dev: false + + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + + /@types/lodash.memoize@4.1.7: + resolution: {integrity: sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==} + dependencies: + '@types/lodash': 4.14.194 + dev: false + + /@types/lodash@4.14.194: + resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} + 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==} + dev: false + + /@types/prismjs@1.26.0: + resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} + dev: true + + /@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-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-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-transition-group@4.4.6: + resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} + dependencies: + '@types/react': 18.2.6 + dev: false + + /@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/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==} + dev: false + + /@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/yargs-parser@21.0.0: + resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: false + + /@types/yargs@17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: false + + /@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) + transitivePeerDependencies: + - supports-color + dev: true + + /@wojtekmaj/date-utils@1.1.3: + resolution: {integrity: sha512-rHrDuTl1cx5LYo8F4K4HVauVjwzx4LwrKfEk4br4fj4nK8JjJZ8IG6a6pBHkYmPLBQHCOEDwstb0WNXMGsmdOw==} + dev: false + + /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@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + 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-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: false + + /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'} + dev: false + + /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@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 + dev: false + + /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 + + /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.5.0(@babel/core@7.21.8): + resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} + 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.5.0 + '@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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.21.5 + 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) + dev: false + + /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) + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + 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) + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /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'} + + /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'} + dev: false + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: false + + /caniuse-lite@1.0.30001487: + resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==} + + /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 + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: false + + /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 + dev: true + + /ci-info@3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + dev: false + + /cjs-module-lexer@1.2.2: + resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + 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 + dev: false + + /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 + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: false + + /collect-v8-coverage@1.0.1: + resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + dev: false + + /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==} + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /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==} + dev: false + + /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 + + /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 + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + + /dayjs@1.11.7: + resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} + 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 + + /dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: false + + /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'} + dev: false + + /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 + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: false + + /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} + dev: false + + /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-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.21.5 + csstype: 3.1.2 + dev: false + + /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'} + dev: false + + /emoji-mart@5.5.2: + resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: false + + /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'} + dev: false + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /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 + dev: false + + /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 + + /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 + dev: false + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + + /fraction.js@4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + 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.*} + dev: false + + /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'} + dev: false + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: false + + /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 + + /get-user-locale@2.2.1: + resolution: {integrity: sha512-3814zipTZ2MvczOcppEXB3jXu+0HWwj5WmPI6//SeCnUIUaRXu7W4S54eQZTEPadlMZefE+jAlPOn+zY3tD4Qw==} + dependencies: + '@types/lodash.memoize': 4.1.7 + lodash.memoize: 4.1.2 + dev: false + + /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@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 + + /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 + + /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==} + dev: false + + /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 + + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: false + + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: false + + /i18next-browser-languagedetector@7.0.1: + resolution: {integrity: sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + + /i18next@22.4.15: + resolution: {integrity: sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + dev: false + + /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 + dev: false + + /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 + + /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==} + dev: false + + /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 + dev: true + + /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'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-hotkey@0.1.8: + resolution: {integrity: sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==} + dev: false + + /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-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'} + dev: false + + /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'} + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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.5.0 + '@jest/expect': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + '@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.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + 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 + dev: false + + /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 + dev: false + + /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.5.0(@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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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.5.0 + chalk: 4.1.2 + jest-get-type: 29.4.3 + jest-util: 29.5.0 + pretty-format: 29.5.0 + dev: false + + /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.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + jest-mock: 29.5.0 + jest-util: 29.5.0 + dev: false + + /jest-get-type@29.4.3: + resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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} + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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.21.5 + '@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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /jiti@1.18.2: + resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} + hasBin: true + dev: true + + /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 + dev: false + + /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 + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: false + + /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 + + /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 + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: false + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: false + + /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 + + /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 + dev: false + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /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: false + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /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 + + /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 + dev: false + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: false + + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false + + /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 + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /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==} + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: false + + /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 + 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.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 + dev: false + + /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 + dev: false + + /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 + dev: false + + /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'} + dev: false + + /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 + dev: false + + /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'} + + /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 + dev: false + + /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 + + /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 + dev: false + + /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 + dev: false + + /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.6(google-protobuf@3.21.2)(typescript@4.9.5): + resolution: {integrity: sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==} + hasBin: true + peerDependencies: + google-protobuf: ^3.13.0 + typescript: 4.x.x + dependencies: + google-protobuf: 3.21.2 + typescript: 4.9.5 + dev: false + + /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==} + dev: false + + /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 + + /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-calendar@4.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-T5oKXD+KLy/g6bmJJkZ7E9wj0iRMesWMZcrC7q2kI6ybOsu9NlPQx8uXJzG4A4C3Sh5Xi0deznyzWIVsUpF8tA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@types/react': 18.2.6 + '@wojtekmaj/date-utils': 1.1.3 + clsx: 1.2.1 + get-user-locale: 2.2.1 + prop-types: 15.8.1 + react: 18.2.0 + 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 + dev: false + + /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-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==} + dev: false + + /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.21.5 + '@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-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 + + /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 + 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 + dev: true + + /redux-thunk@2.4.2(redux@4.2.1): + resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} + peerDependencies: + redux: ^4 + dependencies: + redux: 4.2.1 + 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 + + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: false + + /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'} + dev: false + + /reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + 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 + dev: false + + /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'} + dev: false + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: false + + /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@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 + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + dependencies: + compute-scroll-into-view: 1.0.20 + 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 + + /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==} + dev: false + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /slate-react@0.94.2(react-dom@18.2.0)(react@18.2.0)(slate@0.94.1): + resolution: {integrity: sha512-4wDSuTuGBkdQ609CS55uc2Yhfa5but21usBgAtCVhPJQazL85kzN2vUUYTmGb7d/mpP9tdnJiVPopIyhqlRJ8Q==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + slate: '>=0.65.3' + dependencies: + '@juggle/resize-observer': 3.4.0 + '@types/is-hotkey': 0.1.7 + '@types/lodash': 4.14.194 + direction: 1.0.4 + is-hotkey: 0.1.8 + 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: 2.2.31 + slate: 0.94.1 + tiny-invariant: 1.0.6 + dev: false + + /slate@0.94.1: + resolution: {integrity: sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==} + dependencies: + immer: 9.0.21 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + dev: false + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: false + + /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'} + dev: false + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: false + + /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 + dev: false + + /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: false + + /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@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: false + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: false + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /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 + dev: false + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /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 + dev: false + + /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.0.6: + resolution: {integrity: sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==} + dev: false + + /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 + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /ts-results@3.3.0: + resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==} + dev: false + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + dev: false + + /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'} + dev: false + + /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'} + dev: false + + /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 + + /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 + + /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 + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.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-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 + dev: false + + /vite@4.3.5(@types/node@18.16.9): + 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 + 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 + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: false + + /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 + dev: false + + /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 + dev: false + + /y-indexeddb@9.0.11(yjs@13.6.1): + resolution: {integrity: sha512-HOKQ70qW1h2WJGtOKu9rE8fbX86ExVZedecndMuhwax3yM4DQsQzCTGHt/jvTrFZr/9Ahvd8neD6aZ4dMMjtdg==} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.74 + yjs: 13.6.1 + dev: false + + /y-protocols@1.0.5: + resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==} + dependencies: + lib0: 0.2.74 + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + + /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'} + dev: false + + /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 + dev: false + + /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 + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} 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/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/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..f4dfb82b2c --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock new file mode 100644 index 0000000000..74f9243974 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -0,0 +1,6917 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[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 = "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[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 = "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 = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "appflowy-integrate" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "anyhow", + "collab", + "collab-database", + "collab-document", + "collab-folder", + "collab-persistence", + "collab-plugins", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "appflowy_tauri" +version = "0.0.0" +dependencies = [ + "bytes", + "flowy-core", + "flowy-net", + "flowy-notification", + "lib-dispatch", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-utils", + "tracing", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868f09ff8cea88b079da74ae569d9b8c62a23c68c746240b704ee6f7525c89c" + +[[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.18", +] + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags", + "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.0", +] + +[[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 = "aws-config" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcdcf0d683fe9c23d32cf5b53c9918ea0a500375a9fb20109802552658e576c9" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-sdk-sso", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http", + "hyper", + "ring", + "time 0.3.22", + "tokio", + "tower", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcdb2f7acbc076ff5ad05e7864bdb191ca70a6fd07668dc3a1a8bcd051de5ae" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "fastrand", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-endpoint" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cce1c41a6cfaa726adee9ebb9a56fcd2bbfd8be49fd8a04c5e20fd968330b04" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "aws-types", + "http", + "regex", + "tracing", +] + +[[package]] +name = "aws-http" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aadbc44e7a8f3e71c8b374e03ecd972869eb91dd2bc89ed018954a52ba84bc44" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-types", + "aws-types", + "bytes", + "http", + "http-body", + "lazy_static", + "percent-encoding", + "pin-project-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-dynamodb" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fb64867fe098cffee7e34352b01bbfa2beb3aa1b2ff0e0a7bf9ff293557852" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http", + "regex", + "tokio-stream", + "tower", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8b812340d86d4a766b2ca73f740dfd47a97c2dff0c06c8517a16d88241957e4" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes", + "http", + "regex", + "tokio-stream", + "tower", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265fac131fbfc188e5c3d96652ea90ecc676a934e3174eaaee523c6cec040b3b" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "http", + "regex", + "tower", + "tracing", +] + +[[package]] +name = "aws-sig-auth" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b94acb10af0c879ecd5c7bdf51cda6679a0a4f4643ce630905a77673bfa3c61" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-http", + "aws-types", + "http", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c" +dependencies = [ + "aws-smithy-http", + "form_urlencoded", + "hex", + "hmac", + "http", + "once_cell", + "percent-encoding", + "regex", + "sha2", + "time 0.3.22", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", + "tokio-stream", +] + +[[package]] +name = "aws-smithy-client" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a86aa6e21e86c4252ad6a0e3e74da9617295d8d6e374d552be7d3059c41cedd" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-types", + "bytes", + "fastrand", + "http", + "http-body", + "hyper", + "hyper-rustls 0.23.2", + "lazy_static", + "pin-project-lite", + "rustls 0.20.8", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28" +dependencies = [ + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http", + "http-body", + "hyper", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "aws-smithy-http-tower" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f9f42fbfa96d095194a632fbac19f60077748eba536eb0b9fecc28659807f8" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98819eb0b04020a1c791903533b638534ae6c12e2aceda3e6e6fba015608d51d" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-types" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a3d0bf4f324f4ef9793b86a1701d9700fbcdbd12a846da45eed104c634c6e8" +dependencies = [ + "base64-simd", + "itoa 1.0.6", + "num-integer", + "ryu", + "time 0.3.22", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b9d12875731bd07e767be7baad95700c3137b56730ec9ddeedb52a5e5ca63b" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "0.55.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd209616cc8d7bfb82f87811a5c655dc97537f592689b18743bddf5dc5c4829" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-types", + "http", + "rustc_version", + "tracing", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[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", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.18", +] + +[[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 = "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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[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.13.2", +] + +[[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.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + +[[package]] +name = "bytes-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" +dependencies = [ + "bytes", + "either", +] + +[[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", + "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.0", +] + +[[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.4", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[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 7.1.3", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70d3ad08698a0568b0562f22710fe6bfc1f4a61a367c77d0398c562eadd453a" +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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[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.1", +] + +[[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.1", + "phf_codegen 0.11.1", +] + +[[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 = "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", + "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", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "collab" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "anyhow", + "bytes", + "lib0", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "y-sync", + "yrs", +] + +[[package]] +name = "collab-client-ws" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "bytes", + "collab-sync", + "futures-util", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-tungstenite 0.18.0", + "tracing", +] + +[[package]] +name = "collab-database" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.2", + "chrono", + "collab", + "collab-derive", + "collab-persistence", + "collab-plugins", + "lazy_static", + "lru", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "collab-derive" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", + "yrs", +] + +[[package]] +name = "collab-document" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "anyhow", + "collab", + "collab-derive", + "collab-persistence", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "collab-folder" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "anyhow", + "chrono", + "collab", + "collab-derive", + "collab-persistence", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "collab-persistence" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "bincode", + "chrono", + "lazy_static", + "lib0", + "parking_lot 0.12.1", + "rocksdb", + "serde", + "sled", + "smallvec", + "thiserror", + "tokio", + "tracing", + "yrs", +] + +[[package]] +name = "collab-plugins" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "anyhow", + "async-trait", + "aws-config", + "aws-credential-types", + "aws-sdk-dynamodb", + "base64 0.21.2", + "collab", + "collab-client-ws", + "collab-persistence", + "collab-sync", + "futures-util", + "parking_lot 0.12.1", + "postgrest", + "rand 0.8.5", + "rusoto_credential", + "serde", + "serde_json", + "similar 2.2.1", + "thiserror", + "tokio", + "tokio-retry", + "tracing", + "y-sync", + "yrs", +] + +[[package]] +name = "collab-sync" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +dependencies = [ + "bytes", + "collab", + "futures-util", + "lib0", + "md5", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "y-sync", + "yrs", +] + +[[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 = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom 5.1.3", + "serde", + "yaml-rust", +] + +[[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 = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[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", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags", + "core-foundation", + "foreign-types", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +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 = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "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-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.18", +] + +[[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.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[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", + "syn 2.0.18", +] + +[[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.18", +] + +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.8", +] + +[[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 = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d" +dependencies = [ + "byteorder", + "diesel_derives", + "libsqlite3-sys", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diesel_migrations" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" +dependencies = [ + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[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-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 = "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 = "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.4", + "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 = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-chain" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02" +dependencies = [ + "backtrace", +] + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags", + "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 = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[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 = "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-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-config" +version = "0.1.0" +dependencies = [ + "appflowy-integrate", + "bytes", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-server", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "strum_macros", +] + +[[package]] +name = "flowy-core" +version = "0.1.0" +dependencies = [ + "appflowy-integrate", + "bytes", + "diesel", + "flowy-config", + "flowy-database2", + "flowy-document2", + "flowy-error", + "flowy-folder2", + "flowy-net", + "flowy-server", + "flowy-sqlite", + "flowy-task", + "flowy-user", + "futures-core", + "lib-dispatch", + "lib-infra", + "lib-log", + "lib-ws", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "flowy-database2" +version = "0.1.0" +dependencies = [ + "anyhow", + "appflowy-integrate", + "async-stream", + "async-trait", + "bytes", + "chrono", + "chrono-tz 0.8.2", + "collab", + "collab-database", + "csv", + "dashmap", + "fancy-regex 0.10.0", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-task", + "futures", + "indexmap", + "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", + "tokio", + "tracing", + "url", +] + +[[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-document2" +version = "0.1.0" +dependencies = [ + "anyhow", + "appflowy-integrate", + "bytes", + "collab", + "collab-document", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "indexmap", + "lib-dispatch", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "serde", + "serde_json", + "strum", + "strum_macros", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "flowy-error" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "collab-database", + "collab-document", + "flowy-codegen", + "flowy-derive", + "flowy-sqlite", + "http-error-code", + "lib-dispatch", + "protobuf", + "r2d2", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", +] + +[[package]] +name = "flowy-folder2" +version = "0.1.0" +dependencies = [ + "appflowy-integrate", + "bytes", + "chrono", + "collab", + "collab-folder", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "strum", + "strum_macros", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", +] + +[[package]] +name = "flowy-net" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "lib-dispatch", + "protobuf", + "strum_macros", + "thiserror", + "tracing", +] + +[[package]] +name = "flowy-notification" +version = "0.1.0" +dependencies = [ + "bytes", + "flowy-codegen", + "flowy-derive", + "lazy_static", + "lib-dispatch", + "protobuf", + "serde", + "tracing", +] + +[[package]] +name = "flowy-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "config", + "flowy-error", + "flowy-folder2", + "flowy-user", + "futures-util", + "hyper", + "lazy_static", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "postgrest", + "reqwest", + "serde", + "serde-aux", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tracing", + "uuid", +] + +[[package]] +name = "flowy-sqlite" +version = "0.1.0" +dependencies = [ + "anyhow", + "diesel", + "diesel_derives", + "diesel_migrations", + "error-chain", + "lazy_static", + "libsqlite3-sys", + "parking_lot 0.12.1", + "r2d2", + "scheduled-thread-pool", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "flowy-task" +version = "0.1.0" +dependencies = [ + "anyhow", + "atomic_refcell", + "lib-infra", + "tokio", + "tracing", +] + +[[package]] +name = "flowy-user" +version = "0.1.0" +dependencies = [ + "appflowy-integrate", + "bytes", + "diesel", + "diesel_derives", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-sqlite", + "lazy_static", + "lib-dispatch", + "lib-infra", + "log", + "once_cell", + "parking_lot 0.12.1", + "protobuf", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros", + "tokio", + "tracing", + "unicode-segmentation", + "validator", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +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", + "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", + "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.0", +] + +[[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.0", +] + +[[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.0", +] + +[[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.0", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e123d9ae7c02966b4d892e550bdc32164f05853cd40ab570650ad600596a8a" +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 = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags", + "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.0", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags", + "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.0", +] + +[[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", + "ignore", + "walkdir", +] + +[[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.0", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags", + "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.0", +] + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "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.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + +[[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 = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[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 0.10.7", +] + +[[package]] +name = "html5ever" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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-error-code" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Server?branch=refactor/appflowy_server#1ccd296de8530760d92652dbd9f38f27178059b6" +dependencies = [ + "serde", + "serde_repr", + "thiserror", +] + +[[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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.6", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "log", + "rustls 0.20.8", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +dependencies = [ + "http", + "hyper", + "rustls 0.21.2", + "tokio", + "tokio-rustls 0.24.1", +] + +[[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.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "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 = "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 = "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 = "infer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3" +dependencies = [ + "cfb", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + +[[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 = "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", + "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.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "kuchiki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" +dependencies = [ + "cssparser", + "html5ever", + "matches", + "selectors", +] + +[[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 = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec 0.5.2", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "lib-dispatch" +version = "0.1.0" +dependencies = [ + "bincode", + "bytes", + "derivative", + "dyn-clone", + "futures", + "futures-channel", + "futures-core", + "futures-util", + "log", + "nanoid", + "pin-project", + "protobuf", + "serde", + "serde_json", + "serde_repr", + "thread-id", + "tokio", + "tracing", +] + +[[package]] +name = "lib-infra" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "futures-core", + "md5", + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "lib-log" +version = "0.1.0" +dependencies = [ + "chrono", + "lazy_static", + "log", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-bunyan-formatter", + "tracing-core", + "tracing-log", + "tracing-subscriber 0.2.25", +] + +[[package]] +name = "lib-ws" +version = "0.1.0" +dependencies = [ + "bytes", + "dashmap", + "futures", + "futures-channel", + "futures-core", + "futures-util", + "lib-infra", + "log", + "parking_lot 0.12.1", + "pin-project", + "protobuf", + "serde", + "serde_json", + "serde_repr", + "strum_macros", + "tokio", + "tokio-tungstenite 0.15.0", + "tracing", + "url", +] + +[[package]] +name = "lib0" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf23122cb1c970b77ea6030eac5e328669415b65d2ab245c99bfb110f9d62dc" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[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.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" +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 = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber 0.3.17", +] + +[[package]] +name = "lru" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03f1160296536f10c833a82dca22267d5486734230d47bf00bf435885814ba1e" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[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 = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + +[[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 = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[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 = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +dependencies = [ + "diesel", +] + +[[package]] +name = "migrations_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[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", + "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 = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + +[[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 = "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-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 0.2.6", + "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_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 = "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.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" +dependencies = [ + "bitflags", + "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.18", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags", + "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.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.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", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "pest_meta" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[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_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared 0.11.1", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +dependencies = [ + "phf_generator 0.11.1", + "phf_shared 0.11.1", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared 0.11.1", + "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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.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", + "uncased", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[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.2", + "indexmap", + "line-wrap", + "quick-xml", + "serde", + "time 0.3.22", +] + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.1", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +dependencies = [ + "proc-macro2", + "syn 2.0.18", +] + +[[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.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[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 = "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 = "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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[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", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls 0.24.0", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.2", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg 0.10.1", +] + +[[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 = "rusoto_credential" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" +dependencies = [ + "async-trait", + "chrono", + "dirs-next", + "futures", + "hyper", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rust_decimal" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bd36b60561ee1fb5ec2817f198b6fd09fa571c897a5e86d1487cfc2b096dfc" +dependencies = [ + "arrayvec 0.7.3", + "borsh", + "bytecheck", + "byteorder", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e773fd3da1ed42472fdf3cfdb4972948a555bc3d73f5e0bdb99d17e7b54c687" +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.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[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", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +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 = "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", + "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", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-aux" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3dfe1b7eb6f9dcf011bd6fad169cdeaae75eda0d61b1a99a3f015b41b0cae39" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa 1.0.6", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_spanned" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +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.2", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.22", +] + +[[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.18", +] + +[[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 = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[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 = "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 = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags", + "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", + "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 = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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 = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" + +[[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 = "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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" +dependencies = [ + "cfg-expr 0.15.2", + "heck 0.4.1", + "pkg-config", + "toml 0.7.4", + "version-compare 0.1.1", +] + +[[package]] +name = "tao" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6d198e01085564cea63e976ad1566c1ba2c2e4cc79578e35d9f05521505e31" +dependencies = [ + "bitflags", + "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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" + +[[package]] +name = "tauri" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc35893c7e08d9564a9206bd52182dce031b0d5132dc946b3e166e00d03f8cfe" +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", + "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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b" +dependencies = [ + "anyhow", + "cargo_toml", + "heck 0.4.1", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a" +dependencies = [ + "base64 0.21.2", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "regex", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time 0.3.22", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb12a2454e747896929338d93b0642144bb51e0dddbb36e579035731f0d76b7" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "108683199cb18f96d2d4134187bb789964143c845d2d154848dda209191fd769" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7aa256a1407a3a091b5d843eccc1a5042289baf0a43d1179d9f0fcfea37c1b" +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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.4.1", + "html5ever", + "infer", + "json-patch", + "kuchiki", + "memchr", + "phf 0.10.1", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows 0.39.0", +] + +[[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.4", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[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.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[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 = "tokio" +version = "1.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +dependencies = [ + "autocfg", + "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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[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.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.8", + "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.2", + "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.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite 0.14.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.18.0", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[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.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9965507e507f12c8901432a33e31131222abac31edd90cabbcf85cf544b7127a" +dependencies = [ + "chrono", + "crossbeam-channel", + "tracing-subscriber 0.2.25", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tracing-bunyan-formatter" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c408910c9b7eabc0215fe2b4a89f8ec95581a91cea1f7619f7c78caf14cbc2a1" +dependencies = [ + "chrono", + "gethostname", + "log", + "serde", + "serde_json", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber 0.2.25", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +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-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.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers 0.0.1", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers 0.1.0", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[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 = "tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha-1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "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 = "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 = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna 0.4.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + +[[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.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" +dependencies = [ + "getrandom 0.2.10", + "sha1_smol", +] + +[[package]] +name = "validator" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591" +dependencies = [ + "idna 0.2.3", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + +[[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 = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[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.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +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.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[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.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[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", + "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", + "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.0", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[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.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", +] + +[[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-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", +] + +[[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-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[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_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_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_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_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_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_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 = "winnow" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[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 = "wry" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33748f35413c8a98d45f7a08832d848c0c5915501803d1faade5a4ebcd258cea" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "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 = "xmlparser" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" + +[[package]] +name = "y-sync" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54d34b68ec4514a0659838c2b1ba867c571b20b3804a1338dacf4fa9062d801" +dependencies = [ + "lib0", + "thiserror", + "yrs", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yrs" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2aef2bf89b4f7c003f9c73f1c8097427ca32e1d006443f3f607f11e79a797b" +dependencies = [ + "atomic_refcell", + "lib0", + "rand 0.7.3", + "smallstr", + "smallvec", + "thiserror", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[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..d129b2fee6 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -0,0 +1,52 @@ +[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.2", features = [] } + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +tauri = { version = "1.2", features = ["shell-open"] } +tauri-utils = "1.2" +bytes = { version = "1.4" } +tracing = { version = "0.1", features = ["log"] } +lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = ["use_serde"] } +flowy-core = { path = "../../rust-lib/flowy-core", features = ["rev-sqlite", "ts"] } +flowy-notification = { path = "../../rust-lib/flowy-notification", features = ["ts"] } +flowy-net = { path = "../../rust-lib/flowy-net" } + +[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 = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } + +#collab = { path = "../../AppFlowy-Collab/collab" } +#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" } +#collab-document = { path = "../../AppFlowy-Collab/collab-document" } +#collab-database = { path = "../../AppFlowy-Collab/collab-database" } +#appflowy-integrate = { path = "../../AppFlowy-Collab/appflowy-integrate" } + + + + 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/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..f400973ca7 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.70" 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..3043d0c785 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -0,0 +1,17 @@ +use flowy_core::{AppFlowyCore, AppFlowyCoreConfig, DEFAULT_NAME}; + +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 mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); + if cfg!(debug_assertions) { + data_path.push("dev"); + } + data_path.push("data"); + + std::env::set_var("RUST_LOG", "trace"); + let config = AppFlowyCoreConfig::new(data_path.to_str().unwrap(), DEFAULT_NAME.to_string()) + .log_filter("trace", vec!["appflowy_tauri".to_string()]); + AppFlowyCore::new(config) +} 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..ecee588521 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/src/main.rs @@ -0,0 +1,41 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +mod init; +mod notification; +mod request; + +use flowy_notification::register_notification_sender; +use init::*; +use notification::*; +use request::*; +use tauri::Manager; + +fn main() { + 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(); + 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| { + #[cfg(debug_assertions)] + { + let window = app.get_window("main").unwrap(); + window.open_devtools(); + } + 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..4f88885a51 --- /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, 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..5d60dc72c7 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -0,0 +1,71 @@ +{ + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "pnpm run build", + "devPath": "http://localhost:1420", + "distDir": "../dist", + "withGlobalTauri": false + }, + "package": { + "productName": "AppFlowy", + "version": "0.0.0" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false, + "open": 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": [ + { + "fullscreen": false, + "height": 1200, + "resizable": true, + "title": "AppFlowy", + "width": 1200 + } + ] + } +} 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..9589cfa2d6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/App.tsx @@ -0,0 +1,47 @@ +import { Routes, Route, BrowserRouter } from 'react-router-dom'; + +import { ColorPalette } from './components/tests/ColorPalette'; +import { Provider } from 'react-redux'; +import { store } from './stores/store'; +import { DocumentPage } from './views/DocumentPage'; +import { BoardPage } from './views/BoardPage'; +import { GridPage } from './views/GridPage'; +import { LoginPage } from './views/LoginPage'; +import { ProtectedRoutes } from './components/auth/ProtectedRoutes'; +import { SignUpPage } from './views/SignUpPage'; +import { ConfirmAccountPage } from './views/ConfirmAccountPage'; +import { ErrorHandlerPage } from './components/error/ErrorHandlerPage'; +import initializeI18n from './stores/i18n/initializeI18n'; +import { TestAPI } from './components/tests/TestAPI'; +import { GetStarted } from './components/auth/GetStarted/GetStarted'; +import { ErrorBoundary } from 'react-error-boundary'; +import { AllIcons } from '$app/components/tests/AllIcons'; + +initializeI18n(); + +const App = () => { + return ( + + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + }> + }> + }> + }> + + + + + ); +}; + +export default App; diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/launch_splash.jpg b/frontend/appflowy_tauri/src/appflowy_app/assets/launch_splash.jpg new file mode 100644 index 0000000000..7e3bb9cee6 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/launch_splash.jpg differ 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/components/_shared/Button.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Button.tsx new file mode 100644 index 0000000000..cb637e7f1c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Button.tsx @@ -0,0 +1,45 @@ +import { MouseEventHandler, MouseEvent, ReactNode, useEffect, useState } from 'react'; + +export const Button = ({ + size = 'primary', + children, + onClick, +}: { + size?: 'primary' | 'medium' | 'small' | 'box-small-transparent' | 'medium-transparent'; + children: ReactNode; + onClick?: MouseEventHandler; +}) => { + const [cls, setCls] = useState(''); + useEffect(() => { + switch (size) { + case 'primary': + setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-main-accent text-white'); + break; + case 'medium': + setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-main-accent text-white'); + break; + case 'small': + setCls('w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-main-accent text-white text-xs'); + break; + case 'medium-transparent': + setCls( + 'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-main-accent text-main-accent transition-colors duration-300 hover:bg-main-hovered hover:text-white' + ); + break; + case 'box-small-transparent': + setCls('text-black hover:text-main-accent w-[24px] h-[24px]'); + break; + } + }, [size]); + + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + onClick && onClick(e); + }; + + return ( + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/CheckListProgress.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/CheckListProgress.tsx new file mode 100644 index 0000000000..bea90b64eb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/CheckListProgress.tsx @@ -0,0 +1,27 @@ +export const CheckListProgress = ({ completed, max }: { completed: number; max: number }) => { + return ( +
+ {max > 0 && ( + <> +
+ {completed > 0 && filledCheckListBars({ amount: completed })} + {max - completed > 0 && emptyCheckListBars({ amount: max - completed })} +
+
{((100 * completed) / max).toFixed(0)}%
+ + )} +
+ ); +}; + +const filledCheckListBars = ({ amount }: { amount: number }) => { + return Array(amount) + .fill(0) + .map((item, index) =>
); +}; + +const emptyCheckListBars = ({ amount }: { amount: number }) => { + return Array(amount) + .fill(0) + .map((item, index) =>
); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts new file mode 100644 index 0000000000..50de845116 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts @@ -0,0 +1,48 @@ +import { useAppDispatch, useAppSelector } from '../../stores/store'; +import { useEffect, useState } from 'react'; +import { databaseActions, IDatabase } from '../../stores/reducers/database/slice'; +import { nanoid } from 'nanoid'; +import { FieldType } from '../../../services/backend'; + +export const useDatabase = () => { + const dispatch = useAppDispatch(); + const database = useAppSelector((state) => state.database); + + const newField = () => { + /* dispatch( + databaseActions.addField({ + field: { + fieldId: nanoid(8), + fieldType: FieldType.RichText, + fieldOptions: {}, + title: 'new field', + }, + }) + );*/ + console.log('depreciated'); + }; + + const renameField = (fieldId: string, newTitle: string) => { + /* const field = database.fields[fieldId]; + field.title = newTitle; + + dispatch( + databaseActions.updateField({ + field, + }) + );*/ + console.log('depreciated'); + }; + + const newRow = () => { + // dispatch(databaseActions.addRow()); + console.log('depreciated'); + }; + + return { + database, + newField, + renameField, + newRow, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx new file mode 100644 index 0000000000..e55b6bf70d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx @@ -0,0 +1,48 @@ +import { FieldType } from '@/services/backend'; +import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon'; +import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; + +const typesOrder: FieldType[] = [ + FieldType.RichText, + FieldType.Number, + FieldType.DateTime, + FieldType.SingleSelect, + FieldType.MultiSelect, + FieldType.Checkbox, + FieldType.URL, + FieldType.Checklist, +]; + +export const ChangeFieldTypePopup = ({ + top, + left, + onClick, + onOutsideClick, +}: { + top: number; + left: number; + onClick: (newType: FieldType) => void; + onOutsideClick: () => void; +}) => { + return ( + +
+ {typesOrder.map((t, i) => ( + + ))} +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckList.tsx new file mode 100644 index 0000000000..b008191555 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckList.tsx @@ -0,0 +1,40 @@ +import { SelectOptionCellDataPB } from '@/services/backend'; +import { useEffect, useRef, useState } from 'react'; +import { ISelectOptionType } from '$app_reducers/database/slice'; +import { useAppSelector } from '$app/stores/store'; +import { CheckListProgress } from '$app/components/_shared/CheckListProgress'; + +export const CheckList = ({ + data, + fieldId, + onEditClick, +}: { + data: SelectOptionCellDataPB | undefined; + fieldId: string; + onEditClick: (left: number, top: number) => void; +}) => { + const ref = useRef(null); + const [allOptionsCount, setAllOptionsCount] = useState(0); + const [selectedOptionsCount, setSelectedOptionsCount] = useState(0); + const databaseStore = useAppSelector((state) => state.database); + + useEffect(() => { + setAllOptionsCount((databaseStore.fields[fieldId]?.fieldOptions as ISelectOptionType)?.selectOptions?.length ?? 0); + }, [databaseStore, fieldId]); + + useEffect(() => { + setSelectedOptionsCount((data as SelectOptionCellDataPB)?.select_options?.length ?? 0); + }, [data]); + + const onClick = () => { + if (!ref.current) return; + const { left, top } = ref.current.getBoundingClientRect(); + onEditClick(left, top); + }; + + return ( +
+ +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListOption.tsx new file mode 100644 index 0000000000..380898a35d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListOption.tsx @@ -0,0 +1,60 @@ +import { SelectOptionPB } from '@/services/backend'; +import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg'; +import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg'; +import { Details2Svg } from '$app/components/_shared/svg/Details2Svg'; +import { ISelectOption } from '$app_reducers/database/slice'; +import { MouseEventHandler } from 'react'; + +export const CheckListOption = ({ + option, + checked, + onToggleOptionClick, + openCheckListDetail, +}: { + option: ISelectOption; + checked: boolean; + onToggleOptionClick: (v: SelectOptionPB) => void; + openCheckListDetail: (left: number, top: number, option: SelectOptionPB) => void; +}) => { + const onCheckListDetailClick: MouseEventHandler = (e) => { + e.stopPropagation(); + let target = e.target as HTMLElement; + + while (!(target instanceof HTMLButtonElement)) { + if (target.parentElement === null) return; + target = target.parentElement; + } + + const selectOption = new SelectOptionPB({ + id: option.selectOptionId, + name: option.title, + }); + + const { right: _left, top: _top } = target.getBoundingClientRect(); + openCheckListDetail(_left, _top, selectOption); + }; + + return ( +
+ onToggleOptionClick( + new SelectOptionPB({ + id: option.selectOptionId, + name: option.title, + }) + ) + } + > +
+ {checked ? : } +
+
{option.title}
+
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListPopup.tsx new file mode 100644 index 0000000000..9b3424858d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListPopup.tsx @@ -0,0 +1,97 @@ +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { SelectOptionCellDataPB, SelectOptionPB } from '@/services/backend'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; +import { ISelectOptionType } from '$app_reducers/database/slice'; +import { useAppSelector } from '$app/stores/store'; +import { useCell } from '$app/components/_shared/database-hooks/useCell'; +import { CellCache } from '$app/stores/effects/database/cell/cell_cache'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc'; +import { useEffect, useState } from 'react'; +import { CheckListProgress } from '$app/components/_shared/CheckListProgress'; +import { NewCheckListOption } from '$app/components/_shared/EditRow/CheckList/NewCheckListOption'; +import { CheckListOption } from '$app/components/_shared/EditRow/CheckList/CheckListOption'; +import { NewCheckListButton } from '$app/components/_shared/EditRow/CheckList/NewCheckListButton'; + +export const CheckListPopup = ({ + left, + top, + cellIdentifier, + cellCache, + fieldController, + openCheckListDetail, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; + openCheckListDetail: (left: number, top: number, option: SelectOptionPB) => void; + onOutsideClick: () => void; +}) => { + const databaseStore = useAppSelector((state) => state.database); + const { data } = useCell(cellIdentifier, cellCache, fieldController); + + const [allOptionsCount, setAllOptionsCount] = useState(0); + const [selectedOptionsCount, setSelectedOptionsCount] = useState(0); + const [newOptions, setNewOptions] = useState([]); + + useEffect(() => { + setAllOptionsCount( + (databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType)?.selectOptions?.length ?? 0 + ); + }, [databaseStore, cellIdentifier]); + + useEffect(() => { + setSelectedOptionsCount((data as SelectOptionCellDataPB)?.select_options?.length ?? 0); + }, [data]); + + const onToggleOptionClick = async (option: SelectOptionPB) => { + if ((data as SelectOptionCellDataPB)?.select_options?.find((selectedOption) => selectedOption.id === option.id)) { + await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]); + } else { + await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]); + } + }; + + return ( + +
+
+ +
+ +
+ {(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map( + (option, index) => ( + so.id === option.selectOptionId) + } + onToggleOptionClick={onToggleOptionClick} + openCheckListDetail={openCheckListDetail} + > + ) + )} + {newOptions.map((option, index) => ( + + ))} +
+
+
+ +
+
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx new file mode 100644 index 0000000000..b629efc854 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx @@ -0,0 +1,95 @@ +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { KeyboardEventHandler, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SelectOptionPB } from '@/services/backend'; +import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc'; +import { TrashSvg } from '$app/components/_shared/svg/TrashSvg'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; + +export const EditCheckListPopup = ({ + left, + top, + cellIdentifier, + editingSelectOption, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + editingSelectOption: SelectOptionPB; + onOutsideClick: () => void; +}) => { + const inputRef = useRef(null); + const { t } = useTranslation(); + const [value, setValue] = useState(''); + + useEffect(() => { + setValue(editingSelectOption.name); + }, [editingSelectOption]); + + const onKeyDown: KeyboardEventHandler = async (e) => { + if (e.key === 'Enter' && value.length > 0) { + await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value }); + setValue(''); + } + }; + + const onKeyDownWrapper: KeyboardEventHandler = (e) => { + if (e.key === 'Escape') { + onOutsideClick(); + } + }; + + const onBlur = async () => { + const svc = new SelectOptionCellBackendService(cellIdentifier); + await svc.updateOption( + new SelectOptionPB({ + id: editingSelectOption.id, + name: value, + }) + ); + }; + + const onDeleteOptionClick = async () => { + const svc = new SelectOptionCellBackendService(cellIdentifier); + await svc.deleteOption([editingSelectOption]); + onOutsideClick(); + }; + + return ( + { + await onBlur(); + onOutsideClick(); + }} + left={left} + top={top} + > +
+
+ setValue(e.target.value)} + onKeyDown={onKeyDown} + onBlur={() => onBlur()} + /> +
{value.length}/30
+
+ +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListButton.tsx new file mode 100644 index 0000000000..b9aff6a93c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListButton.tsx @@ -0,0 +1,28 @@ +import AddSvg from '$app/components/_shared/svg/AddSvg'; +import { useTranslation } from 'react-i18next'; + +export const NewCheckListButton = ({ + newOptions, + setNewOptions, +}: { + newOptions: string[]; + setNewOptions: (v: string[]) => void; +}) => { + const { t } = useTranslation(); + + const newOptionClick = () => { + setNewOptions([...newOptions, '']); + }; + + return ( + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListOption.tsx new file mode 100644 index 0000000000..7c860b6720 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListOption.tsx @@ -0,0 +1,53 @@ +import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc'; +import { useTranslation } from 'react-i18next'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; + +export const NewCheckListOption = ({ + index, + option, + newOptions, + setNewOptions, + cellIdentifier, +}: { + index: number; + option: string; + newOptions: string[]; + setNewOptions: (v: string[]) => void; + cellIdentifier: CellIdentifier; +}) => { + const { t } = useTranslation(); + + const updateNewOption = (value: string) => { + const newOptionsCopy = [...newOptions]; + newOptionsCopy[index] = value; + setNewOptions(newOptionsCopy); + }; + + const onNewOptionKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + void onSaveNewOptionClick(); + } + }; + + const onSaveNewOptionClick = async () => { + await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: newOptions[index] }); + setNewOptions(newOptions.filter((_, i) => i !== index)); + }; + + return ( +
+ onNewOptionKeyDown(e as unknown as KeyboardEvent)} + className={'min-w-0 flex-1 pl-7'} + value={option} + onChange={(e) => updateNewOption(e.target.value)} + /> + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateFormatPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateFormatPopup.tsx new file mode 100644 index 0000000000..de8d2f2722 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateFormatPopup.tsx @@ -0,0 +1,96 @@ +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; +import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg'; +import { useTranslation } from 'react-i18next'; +import { DateFormatPB } from '@/services/backend'; +import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks'; +import { useAppSelector } from '$app/stores/store'; +import { useEffect, useState } from 'react'; +import { IDateType } from '$app_reducers/database/slice'; + +export const DateFormatPopup = ({ + left, + top, + cellIdentifier, + fieldController, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const { t } = useTranslation(); + const { changeDateFormat } = useDateTimeFormat(cellIdentifier, fieldController); + const databaseStore = useAppSelector((state) => state.database); + const [dateType, setDateType] = useState(); + + useEffect(() => { + setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType); + }, [databaseStore]); + + const changeFormat = async (format: DateFormatPB) => { + await changeDateFormat(format); + onOutsideClick(); + }; + + return ( + + + + + + + ); +}; + +function PopupItem({ + format, + text, + changeFormat, + checked, +}: { + format: DateFormatPB; + text: string; + changeFormat: (_: DateFormatPB) => Promise; + checked: boolean; +}) { + return ( + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DatePickerPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DatePickerPopup.tsx new file mode 100644 index 0000000000..e9eb02af8e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DatePickerPopup.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { CellCache } from '$app/stores/effects/database/cell/cell_cache'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import Calendar from 'react-calendar'; +import dayjs from 'dayjs'; +import { useCell } from '$app/components/_shared/database-hooks/useCell'; +import { CalendarData } from '$app/stores/effects/database/cell/controller_builder'; +import { DateCellDataPB } from '@/services/backend'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; +import { DateTypeOptions } from '$app/components/_shared/EditRow/Date/DateTypeOptions'; + +export const DatePickerPopup = ({ + left, + top, + cellIdentifier, + cellCache, + fieldController, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); + const [selectedDate, setSelectedDate] = useState(new Date()); + + useEffect(() => { + const date_pb = data as DateCellDataPB | undefined; + if (!date_pb || !date_pb?.date.length) return; + + setSelectedDate(dayjs(date_pb.date).toDate()); + }, [data]); + + const onChange = async (v: Date | null | (Date | null)[]) => { + if (v instanceof Date) { + setSelectedDate(v); + const date = new CalendarData(dayjs(v).add(dayjs().utcOffset(), 'minutes').toDate(), false); + await cellController?.saveCellData(date); + } + }; + + return ( + +
+ onChange(d)} value={selectedDate} /> +
+ +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTimeFormat.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTimeFormat.hooks.ts new file mode 100644 index 0000000000..e024881350 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTimeFormat.hooks.ts @@ -0,0 +1,37 @@ +import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller'; +import { Some } from 'ts-results'; +import { DateFormatPB, DateTypeOptionPB, FieldType, TimeFormatPB } from '@/services/backend'; +import { makeDateTypeOptionContext } from '$app/stores/effects/database/field/type_option/type_option_context'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; + +export const useDateTimeFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => { + const changeFormat = async (change: (option: DateTypeOptionPB) => void) => { + const fieldInfo = fieldController.getField(cellIdentifier.fieldId); + if (!fieldInfo) return; + const typeOptionController = new TypeOptionController(cellIdentifier.viewId, Some(fieldInfo), FieldType.DateTime); + await typeOptionController.initialize(); + const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController); + const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); + change(typeOption); + await dateTypeOptionContext.setTypeOption(typeOption); + }; + + const changeDateFormat = async (format: DateFormatPB) => { + await changeFormat((option) => (option.date_format = format)); + }; + const changeTimeFormat = async (format: TimeFormatPB) => { + await changeFormat((option) => (option.time_format = format)); + }; + const includeTime = async (include: boolean) => { + await changeFormat((option) => { + // option.include_time = include; + }); + }; + + return { + changeDateFormat, + changeTimeFormat, + includeTime, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx new file mode 100644 index 0000000000..254382df2e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx @@ -0,0 +1,146 @@ +import { DateFormatPopup } from '$app/components/_shared/EditRow/Date/DateFormatPopup'; +import { TimeFormatPopup } from '$app/components/_shared/EditRow/Date/TimeFormatPopup'; +import { MoreSvg } from '$app/components/_shared/svg/MoreSvg'; +import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg'; +import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg'; +import { MouseEventHandler, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IDateType } from '$app_reducers/database/slice'; +import { useAppSelector } from '$app/stores/store'; +import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; + +export const DateTypeOptions = ({ + cellIdentifier, + fieldController, +}: { + cellIdentifier: CellIdentifier; + fieldController: FieldController; +}) => { + const { t } = useTranslation(); + + const [showDateFormatPopup, setShowDateFormatPopup] = useState(false); + const [dateFormatTop, setDateFormatTop] = useState(0); + const [dateFormatLeft, setDateFormatLeft] = useState(0); + + const [showTimeFormatPopup, setShowTimeFormatPopup] = useState(false); + const [timeFormatTop, setTimeFormatTop] = useState(0); + const [timeFormatLeft, setTimeFormatLeft] = useState(0); + + const [dateType, setDateType] = useState(); + + const databaseStore = useAppSelector((state) => state.database); + const { includeTime } = useDateTimeFormat(cellIdentifier, fieldController); + + useEffect(() => { + setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType); + }, [databaseStore]); + + const onDateFormatClick = (_left: number, _top: number) => { + setShowDateFormatPopup(true); + setDateFormatLeft(_left + 10); + setDateFormatTop(_top); + }; + + const onTimeFormatClick = (_left: number, _top: number) => { + setShowTimeFormatPopup(true); + setTimeFormatLeft(_left + 10); + setTimeFormatTop(_top); + }; + + const _onDateFormatClick: MouseEventHandler = (e) => { + e.stopPropagation(); + let target = e.target as HTMLElement; + + while (!(target instanceof HTMLButtonElement)) { + if (target.parentElement === null) return; + target = target.parentElement; + } + + const { right: _left, top: _top } = target.getBoundingClientRect(); + onDateFormatClick(_left, _top); + }; + + const _onTimeFormatClick: MouseEventHandler = (e) => { + e.stopPropagation(); + let target = e.target as HTMLElement; + + while (!(target instanceof HTMLButtonElement)) { + if (target.parentElement === null) return; + target = target.parentElement; + } + + const { right: _left, top: _top } = target.getBoundingClientRect(); + onTimeFormatClick(_left, _top); + }; + + const toggleIncludeTime = async () => { + // if (dateType?.includeTime) { + // await includeTime(false); + // } else { + // await includeTime(true); + // } + }; + + return ( +
+
+ +
+ + + + {showDateFormatPopup && ( + setShowDateFormatPopup(false)} + > + )} + {showTimeFormatPopup && ( + setShowTimeFormatPopup(false)} + > + )} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/EditCellDate.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/EditCellDate.tsx new file mode 100644 index 0000000000..08f0871d7c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/EditCellDate.tsx @@ -0,0 +1,24 @@ +import { MouseEventHandler, useRef } from 'react'; +import { DateCellDataPB } from '@/services/backend'; + +export const EditCellDate = ({ + data, + onEditClick, +}: { + data?: DateCellDataPB; + onEditClick: (left: number, top: number) => void; +}) => { + const ref = useRef(null); + + const onClick: MouseEventHandler = () => { + if (!ref.current) return; + const { left, top } = ref.current.getBoundingClientRect(); + onEditClick(left, top); + }; + + return ( +
+ {data?.date}  +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/NumberFormat.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/NumberFormat.hooks.ts new file mode 100644 index 0000000000..945074e460 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/NumberFormat.hooks.ts @@ -0,0 +1,23 @@ +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { FieldType, NumberFormatPB } from '@/services/backend'; +import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller'; +import { Some } from 'ts-results'; +import { makeNumberTypeOptionContext } from '$app/stores/effects/database/field/type_option/type_option_context'; + +export const useNumberFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => { + const changeNumberFormat = async (format: NumberFormatPB) => { + const fieldInfo = fieldController.getField(cellIdentifier.fieldId); + if (!fieldInfo) return; + const typeOptionController = new TypeOptionController(cellIdentifier.viewId, Some(fieldInfo), FieldType.Number); + await typeOptionController.initialize(); + const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController); + const typeOption = await numberTypeOptionContext.getTypeOption().then((a) => a.unwrap()); + typeOption.format = format; + await numberTypeOptionContext.setTypeOption(typeOption); + }; + + return { + changeNumberFormat, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/NumberFormatPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/NumberFormatPopup.tsx new file mode 100644 index 0000000000..2c1502e22f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/NumberFormatPopup.tsx @@ -0,0 +1,108 @@ +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; +import { useNumberFormat } from '$app/components/_shared/EditRow/Date/NumberFormat.hooks'; +import { NumberFormatPB } from '@/services/backend'; +import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg'; +import { useAppSelector } from '$app/stores/store'; +import { useEffect, useState } from 'react'; +import { INumberType } from '$app_reducers/database/slice'; + +const list = [ + { format: NumberFormatPB.Num, title: 'Num' }, + { format: NumberFormatPB.USD, title: 'USD' }, + { format: NumberFormatPB.CanadianDollar, title: 'CanadianDollar' }, + { format: NumberFormatPB.EUR, title: 'EUR' }, + { format: NumberFormatPB.Pound, title: 'Pound' }, + { format: NumberFormatPB.Yen, title: 'Yen' }, + { format: NumberFormatPB.Ruble, title: 'Ruble' }, + { format: NumberFormatPB.Rupee, title: 'Rupee' }, + { format: NumberFormatPB.Won, title: 'Won' }, + { format: NumberFormatPB.Yuan, title: 'Yuan' }, + { format: NumberFormatPB.Real, title: 'Real' }, + { format: NumberFormatPB.Lira, title: 'Lira' }, + { format: NumberFormatPB.Rupiah, title: 'Rupiah' }, + { format: NumberFormatPB.Franc, title: 'Franc' }, + { format: NumberFormatPB.HongKongDollar, title: 'HongKongDollar' }, + { format: NumberFormatPB.NewZealandDollar, title: 'NewZealandDollar' }, + { format: NumberFormatPB.Krona, title: 'Krona' }, + { format: NumberFormatPB.NorwegianKrone, title: 'NorwegianKrone' }, + { format: NumberFormatPB.MexicanPeso, title: 'MexicanPeso' }, + { format: NumberFormatPB.Rand, title: 'Rand' }, + { format: NumberFormatPB.NewTaiwanDollar, title: 'NewTaiwanDollar' }, + { format: NumberFormatPB.DanishKrone, title: 'DanishKrone' }, + { format: NumberFormatPB.Baht, title: 'Baht' }, + { format: NumberFormatPB.Forint, title: 'Forint' }, + { format: NumberFormatPB.Koruna, title: 'Koruna' }, + { format: NumberFormatPB.Shekel, title: 'Shekel' }, + { format: NumberFormatPB.ChileanPeso, title: 'ChileanPeso' }, + { format: NumberFormatPB.PhilippinePeso, title: 'PhilippinePeso' }, + { format: NumberFormatPB.Dirham, title: 'Dirham' }, + { format: NumberFormatPB.ColombianPeso, title: 'ColombianPeso' }, + { format: NumberFormatPB.Riyal, title: 'Riyal' }, + { format: NumberFormatPB.Ringgit, title: 'Ringgit' }, + { format: NumberFormatPB.Leu, title: 'Leu' }, + { format: NumberFormatPB.ArgentinePeso, title: 'ArgentinePeso' }, + { format: NumberFormatPB.UruguayanPeso, title: 'UruguayanPeso' }, + { format: NumberFormatPB.Percent, title: 'Percent' }, +]; + +export const NumberFormatPopup = ({ + left, + top, + cellIdentifier, + fieldController, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const { changeNumberFormat } = useNumberFormat(cellIdentifier, fieldController); + const databaseStore = useAppSelector((state) => state.database); + const [numberType, setNumberType] = useState(); + + useEffect(() => { + setNumberType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as INumberType); + }, [databaseStore]); + + const changeNumberFormatClick = async (format: NumberFormatPB) => { + await changeNumberFormat(format); + onOutsideClick(); + }; + + return ( + +
+ {list.map((item, index) => ( + changeNumberFormatClick(item.format)} + > + ))} +
+
+ ); +}; + +const FormatButton = ({ title, checked, onClick }: { title: string; checked: boolean; onClick: () => void }) => { + return ( + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/TimeFormatPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/TimeFormatPopup.tsx new file mode 100644 index 0000000000..d12e88ad87 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/TimeFormatPopup.tsx @@ -0,0 +1,72 @@ +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { useTranslation } from 'react-i18next'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; +import { TimeFormatPB } from '@/services/backend'; +import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg'; +import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks'; +import { useAppSelector } from '$app/stores/store'; +import { useEffect, useState } from 'react'; +import { IDateType } from '$app_reducers/database/slice'; + +export const TimeFormatPopup = ({ + left, + top, + cellIdentifier, + fieldController, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const { t } = useTranslation(); + const databaseStore = useAppSelector((state) => state.database); + const [dateType, setDateType] = useState(); + + useEffect(() => { + setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType); + }, [databaseStore]); + + const { changeTimeFormat } = useDateTimeFormat(cellIdentifier, fieldController); + + const changeFormat = async (format: TimeFormatPB) => { + await changeTimeFormat(format); + onOutsideClick(); + }; + + return ( + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellWrapper.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellWrapper.tsx new file mode 100644 index 0000000000..9728043975 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellWrapper.tsx @@ -0,0 +1,122 @@ +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { useCell } from '$app/components/_shared/database-hooks/useCell'; +import { CellCache } from '$app/stores/effects/database/cell/cell_cache'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend'; +import { useAppSelector } from '$app/stores/store'; +import { EditCellText } from '$app/components/_shared/EditRow/InlineEditFields/EditCellText'; +import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon'; +import { EditCellDate } from '$app/components/_shared/EditRow/Date/EditCellDate'; +import { useRef } from 'react'; +import { CellOptions } from '$app/components/_shared/EditRow/Options/CellOptions'; +import { EditCellNumber } from '$app/components/_shared/EditRow/InlineEditFields/EditCellNumber'; +import { EditCheckboxCell } from '$app/components/_shared/EditRow/InlineEditFields/EditCheckboxCell'; +import { EditCellUrl } from '$app/components/_shared/EditRow/InlineEditFields/EditCellUrl'; +import { Draggable } from 'react-beautiful-dnd'; +import { DragElementSvg } from '$app/components/_shared/svg/DragElementSvg'; +import { CheckList } from '$app/components/_shared/EditRow/CheckList/CheckList'; + +export const EditCellWrapper = ({ + index, + cellIdentifier, + cellCache, + fieldController, + onEditFieldClick, + onEditOptionsClick, + onEditDateClick, + onEditCheckListClick, +}: { + index: number; + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; + onEditFieldClick: (cell: CellIdentifier, left: number, top: number) => void; + onEditOptionsClick: (cell: CellIdentifier, left: number, top: number) => void; + onEditDateClick: (cell: CellIdentifier, left: number, top: number) => void; + onEditCheckListClick: (cell: CellIdentifier, left: number, top: number) => void; +}) => { + const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); + const databaseStore = useAppSelector((state) => state.database); + const el = useRef(null); + + const onClick = () => { + if (!el.current) return; + const { top, right } = el.current.getBoundingClientRect(); + onEditFieldClick(cellIdentifier, right, top); + }; + + return ( + + {(provided) => ( +
+
+
onClick()} className={'flex h-5 w-5'}> + +
+ +
+ +
+ + {databaseStore.fields[cellIdentifier.fieldId]?.title ?? ''} + +
+ +
+ {(cellIdentifier.fieldType === FieldType.SingleSelect || + cellIdentifier.fieldType === FieldType.MultiSelect) && + cellController && ( + onEditOptionsClick(cellIdentifier, left, top)} + > + )} + + {cellIdentifier.fieldType === FieldType.Checklist && cellController && ( + onEditCheckListClick(cellIdentifier, left, top)} + > + )} + + {cellIdentifier.fieldType === FieldType.Checkbox && cellController && ( + + )} + + {cellIdentifier.fieldType === FieldType.DateTime && ( + onEditDateClick(cellIdentifier, left, top)} + > + )} + + {cellIdentifier.fieldType === FieldType.Number && cellController && ( + + )} + + {cellIdentifier.fieldType === FieldType.URL && cellController && ( + + )} + + {cellIdentifier.fieldType === FieldType.RichText && cellController && ( + + )} +
+
+ )} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx new file mode 100644 index 0000000000..a17afe1277 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx @@ -0,0 +1,150 @@ +import { FocusEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react'; +import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon'; +import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName'; +import { useTranslation } from 'react-i18next'; +import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller'; +import { Some } from 'ts-results'; +import { FieldController, FieldInfo } from '$app/stores/effects/database/field/field_controller'; +import { MoreSvg } from '$app/components/_shared/svg/MoreSvg'; +import { useAppSelector } from '$app/stores/store'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { PopupWindow } from '$app/components/_shared/PopupWindow'; +import { FieldType } from '@/services/backend'; +import { DateTypeOptions } from '$app/components/_shared/EditRow/Date/DateTypeOptions'; + +export const EditFieldPopup = ({ + top, + left, + cellIdentifier, + viewId, + onOutsideClick, + fieldInfo, + fieldController, + changeFieldTypeClick, + onNumberFormat, +}: { + top: number; + left: number; + cellIdentifier: CellIdentifier; + viewId: string; + onOutsideClick: () => void; + fieldInfo: FieldInfo | undefined; + fieldController?: FieldController; + changeFieldTypeClick: (buttonTop: number, buttonRight: number) => void; + onNumberFormat?: (buttonLeft: number, buttonTop: number) => void; +}) => { + const databaseStore = useAppSelector((state) => state.database); + const { t } = useTranslation(); + const changeTypeButtonRef = useRef(null); + const inputRef = useRef(null); + const [name, setName] = useState(''); + + useEffect(() => { + setName(databaseStore.fields[cellIdentifier.fieldId].title); + }, [databaseStore, cellIdentifier]); + + // focus input on mount + useEffect(() => { + if (!inputRef.current || !name) return; + inputRef.current.focus(); + }, [inputRef, name]); + + const selectAll: FocusEventHandler = (e) => { + e.target.selectionStart = 0; + e.target.selectionEnd = e.target.value.length; + }; + + const save = async () => { + if (!fieldInfo) return; + const controller = new TypeOptionController(viewId, Some(fieldInfo)); + await controller.initialize(); + await controller.setFieldName(name); + }; + + const onChangeFieldTypeClick = () => { + if (!changeTypeButtonRef.current) return; + const { top: buttonTop, right: buttonRight } = changeTypeButtonRef.current.getBoundingClientRect(); + changeFieldTypeClick(buttonTop, buttonRight); + }; + + const onNumberFormatClick: MouseEventHandler = (e) => { + e.stopPropagation(); + let target = e.target as HTMLElement; + + while (!(target instanceof HTMLButtonElement)) { + if (target.parentElement === null) return; + target = target.parentElement; + } + + const { right: _left, top: _top } = target.getBoundingClientRect(); + onNumberFormat?.(_left, _top); + }; + + return ( + { + await save(); + onOutsideClick(); + }} + left={left} + top={top} + > +
+ setName(e.target.value)} + onBlur={() => save()} + className={'border-shades-3 flex-1 rounded border bg-main-selector px-2 py-2'} + /> + +
onChangeFieldTypeClick()} + className={ + 'relative flex cursor-pointer items-center justify-between rounded-lg py-2 text-black hover:bg-main-secondary' + } + > + + + + + + +
+ + {cellIdentifier.fieldType === FieldType.Number && ( + <> +
+ + + )} + + {cellIdentifier.fieldType === FieldType.DateTime && fieldController && ( + + )} +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx new file mode 100644 index 0000000000..673aab5687 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx @@ -0,0 +1,367 @@ +import { CloseSvg } from '$app/components/_shared/svg/CloseSvg'; +import { useRow } from '$app/components/_shared/database-hooks/useRow'; +import { DatabaseController } from '$app/stores/effects/database/database_controller'; +import { RowInfo } from '$app/stores/effects/database/row/row_cache'; +import { EditCellWrapper } from '$app/components/_shared/EditRow/EditCellWrapper'; +import AddSvg from '$app/components/_shared/svg/AddSvg'; +import { useTranslation } from 'react-i18next'; +import { EditFieldPopup } from '$app/components/_shared/EditRow/EditFieldPopup'; +import { useEffect, useState } from 'react'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup'; +import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller'; +import { Some } from 'ts-results'; +import { FieldType, SelectOptionPB } from '@/services/backend'; +import { CellOptionsPopup } from '$app/components/_shared/EditRow/Options/CellOptionsPopup'; +import { DatePickerPopup } from '$app/components/_shared/EditRow/Date/DatePickerPopup'; +import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd'; +import { EditCellOptionPopup } from '$app/components/_shared/EditRow/Options/EditCellOptionPopup'; +import { NumberFormatPopup } from '$app/components/_shared/EditRow/Date/NumberFormatPopup'; +import { CheckListPopup } from '$app/components/_shared/EditRow/CheckList/CheckListPopup'; +import { EditCheckListPopup } from '$app/components/_shared/EditRow/CheckList/EditCheckListPopup'; +import { PropertiesPanel } from '$app/components/_shared/EditRow/PropertiesPanel'; +import { ImageSvg } from '$app/components/_shared/svg/ImageSvg'; +import { PromptWindow } from '$app/components/_shared/PromptWindow'; +import { useAppSelector } from '$app/stores/store'; + +export const EditRow = ({ + onClose, + viewId, + controller, + rowInfo, +}: { + onClose: () => void; + viewId: string; + controller: DatabaseController; + rowInfo: RowInfo; +}) => { + const databaseStore = useAppSelector((state) => state.database); + const { cells, onNewColumnClick } = useRow(viewId, controller, rowInfo); + const { t } = useTranslation(); + const [unveil, setUnveil] = useState(false); + + const [editingCell, setEditingCell] = useState(null); + const [showFieldEditor, setShowFieldEditor] = useState(false); + const [editFieldTop, setEditFieldTop] = useState(0); + const [editFieldLeft, setEditFieldLeft] = useState(0); + + const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false); + const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0); + const [changeFieldTypeLeft, setChangeFieldTypeLeft] = useState(0); + + const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false); + const [changeOptionsTop, setChangeOptionsTop] = useState(0); + const [changeOptionsLeft, setChangeOptionsLeft] = useState(0); + + const [showDatePicker, setShowDatePicker] = useState(false); + const [datePickerTop, setDatePickerTop] = useState(0); + const [datePickerLeft, setDatePickerLeft] = useState(0); + + const [showEditCellOption, setShowEditCellOption] = useState(false); + const [editCellOptionTop, setEditCellOptionTop] = useState(0); + const [editCellOptionLeft, setEditCellOptionLeft] = useState(0); + + const [editingSelectOption, setEditingSelectOption] = useState(); + + const [showEditCheckList, setShowEditCheckList] = useState(false); + const [editCheckListTop, setEditCheckListTop] = useState(0); + const [editCheckListLeft, setEditCheckListLeft] = useState(0); + + const [showNumberFormatPopup, setShowNumberFormatPopup] = useState(false); + const [numberFormatTop, setNumberFormatTop] = useState(0); + const [numberFormatLeft, setNumberFormatLeft] = useState(0); + + const [showCheckListPopup, setShowCheckListPopup] = useState(false); + const [checkListPopupTop, setCheckListPopupTop] = useState(0); + const [checkListPopupLeft, setCheckListPopupLeft] = useState(0); + + const [deletingPropertyId, setDeletingPropertyId] = useState(null); + const [showDeletePropertyPrompt, setShowDeletePropertyPrompt] = useState(false); + + useEffect(() => { + setUnveil(true); + }, []); + + const onCloseClick = () => { + setUnveil(false); + setTimeout(() => { + onClose(); + }, 300); + }; + + const onEditFieldClick = (cellIdentifier: CellIdentifier, left: number, top: number) => { + setEditingCell(cellIdentifier); + setEditFieldTop(top); + setEditFieldLeft(left + 10); + setShowFieldEditor(true); + }; + + const onOutsideEditFieldClick = () => { + if (!showChangeFieldTypePopup) { + setShowFieldEditor(false); + } + }; + + const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => { + setChangeFieldTypeTop(buttonTop); + setChangeFieldTypeLeft(buttonRight + 30); + setShowChangeFieldTypePopup(true); + }; + + const changeFieldType = async (newType: FieldType) => { + if (!editingCell) return; + + const currentField = controller.fieldController.getField(editingCell.fieldId); + if (!currentField) return; + + const typeOptionController = new TypeOptionController(viewId, Some(currentField)); + await typeOptionController.switchToField(newType); + + setEditingCell(new CellIdentifier(viewId, rowInfo.row.id, editingCell.fieldId, newType)); + + setShowChangeFieldTypePopup(false); + }; + + const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => { + setEditingCell(cellIdentifier); + setChangeOptionsLeft(left); + setChangeOptionsTop(top + 40); + setShowChangeOptionsPopup(true); + }; + + const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => { + setEditingCell(cellIdentifier); + setDatePickerLeft(left); + setDatePickerTop(top + 40); + setShowDatePicker(true); + }; + + const onOpenOptionDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => { + setEditingSelectOption(_select_option); + setShowEditCellOption(true); + setEditCellOptionLeft(_left); + setEditCellOptionTop(_top); + }; + + const onOpenCheckListDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => { + setEditingSelectOption(_select_option); + setShowEditCheckList(true); + setEditCheckListLeft(_left + 10); + setEditCheckListTop(_top); + }; + + const onNumberFormat = (_left: number, _top: number) => { + setShowNumberFormatPopup(true); + setNumberFormatLeft(_left + 10); + setNumberFormatTop(_top); + }; + + const onEditCheckListClick = (cellIdentifier: CellIdentifier, left: number, top: number) => { + setEditingCell(cellIdentifier); + setShowCheckListPopup(true); + setCheckListPopupLeft(left); + setCheckListPopupTop(top + 40); + }; + + const onDragEnd: OnDragEndResponder = (result) => { + if (!result.destination?.index) return; + void controller.moveField({ + fieldId: result.draggableId, + fromIndex: result.source.index, + toIndex: result.destination.index, + }); + }; + + const onDeletePropertyClick = (fieldId: string) => { + setDeletingPropertyId(fieldId); + setShowDeletePropertyPrompt(true); + }; + + const onDelete = async () => { + if (!deletingPropertyId) return; + const fieldInfo = controller.fieldController.getField(deletingPropertyId); + if (!fieldInfo) return; + const typeController = new TypeOptionController(viewId, Some(fieldInfo)); + await typeController.initialize(); + await typeController.deleteField(); + setShowDeletePropertyPrompt(false); + }; + + return ( + <> +
onCloseClick()} + > +
{ + e.stopPropagation(); + }} + className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white `} + > +
onCloseClick()} className={'absolute top-1 right-1'}> + +
+ +
+
+
+ +
+ + + + {(provided) => ( +
+ {cells + .filter((cell) => databaseStore.fields[cell.cellIdentifier.fieldId].visible) + .map((cell, cellIndex) => ( + + ))} +
+ )} +
+
+ +
+ +
+
+ +
+ + {showFieldEditor && editingCell && ( + + )} + {showChangeFieldTypePopup && ( + changeFieldType(newType)} + onOutsideClick={() => setShowChangeFieldTypePopup(false)} + > + )} + {showChangeOptionsPopup && editingCell && ( + !showEditCellOption && setShowChangeOptionsPopup(false)} + openOptionDetail={onOpenOptionDetailClick} + > + )} + {showDatePicker && editingCell && ( + setShowDatePicker(false)} + > + )} + {showEditCellOption && editingCell && editingSelectOption && ( + { + setShowEditCellOption(false); + }} + > + )} + {showNumberFormatPopup && editingCell && ( + { + setShowNumberFormatPopup(false); + }} + > + )} + {showCheckListPopup && editingCell && ( + !showEditCheckList && setShowCheckListPopup(false)} + openCheckListDetail={onOpenCheckListDetailClick} + > + )} + {showEditCheckList && editingCell && editingSelectOption && ( + setShowEditCheckList(false)} + > + )} +
+
+ {showDeletePropertyPrompt && ( + onDelete()} + onCancel={() => setShowDeletePropertyPrompt(false)} + > + )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeIcon.tsx new file mode 100644 index 0000000000..8c82601cba --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeIcon.tsx @@ -0,0 +1,24 @@ +import { FieldType } from '@/services/backend'; +import { TextTypeSvg } from '$app/components/_shared/svg/TextTypeSvg'; +import { NumberTypeSvg } from '$app/components/_shared/svg/NumberTypeSvg'; +import { DateTypeSvg } from '$app/components/_shared/svg/DateTypeSvg'; +import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg'; +import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg'; +import { ChecklistTypeSvg } from '$app/components/_shared/svg/ChecklistTypeSvg'; +import { UrlTypeSvg } from '$app/components/_shared/svg/UrlTypeSvg'; +import { CheckboxSvg } from '$app/components/_shared/svg/CheckboxSvg'; + +export const FieldTypeIcon = ({ fieldType }: { fieldType: FieldType }) => { + return ( + <> + {fieldType === FieldType.RichText && } + {fieldType === FieldType.Number && } + {fieldType === FieldType.DateTime && } + {fieldType === FieldType.SingleSelect && } + {fieldType === FieldType.MultiSelect && } + {fieldType === FieldType.Checklist && } + {fieldType === FieldType.URL && } + {fieldType === FieldType.Checkbox && } + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeName.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeName.tsx new file mode 100644 index 0000000000..daeff4cdf0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/FieldTypeName.tsx @@ -0,0 +1,18 @@ +import { FieldType } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; + +export const FieldTypeName = ({ fieldType }: { fieldType: FieldType }) => { + const { t } = useTranslation(); + return ( + <> + {fieldType === FieldType.RichText && t('grid.field.textFieldName')} + {fieldType === FieldType.Number && t('grid.field.numberFieldName')} + {fieldType === FieldType.DateTime && t('grid.field.dateFieldName')} + {fieldType === FieldType.SingleSelect && t('grid.field.singleSelectFieldName')} + {fieldType === FieldType.MultiSelect && t('grid.field.multiSelectFieldName')} + {fieldType === FieldType.Checklist && t('grid.field.checklistFieldName')} + {fieldType === FieldType.URL && t('grid.field.urlFieldName')} + {fieldType === FieldType.Checkbox && t('grid.field.checkboxFieldName')} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellNumber.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellNumber.tsx new file mode 100644 index 0000000000..efe1afd5f0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellNumber.tsx @@ -0,0 +1,29 @@ +import { CellController } from '$app/stores/effects/database/cell/cell_controller'; +import { useEffect, useState } from 'react'; + +export const EditCellNumber = ({ + data, + cellController, +}: { + data: string | undefined; + cellController: CellController; +}) => { + const [value, setValue] = useState(''); + + useEffect(() => { + setValue(data ?? ''); + }, [data]); + + const save = async () => { + await cellController?.saveCellData(value); + }; + + return ( + setValue(e.target.value)} + onBlur={() => save()} + className={'w-full px-4 py-1'} + > + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellText.tsx new file mode 100644 index 0000000000..80247a6f71 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellText.tsx @@ -0,0 +1,42 @@ +import { CellController } from '$app/stores/effects/database/cell/cell_controller'; +import { useEffect, useState } from 'react'; + +export const EditCellText = ({ + data, + cellController, +}: { + data: string | undefined; + cellController: CellController; +}) => { + const [value, setValue] = useState(''); + const [contentRows, setContentRows] = useState(1); + + useEffect(() => { + setValue(data ?? ''); + }, [data]); + + useEffect(() => { + if (!value?.length) return; + setContentRows(Math.max(1, (value ?? '').split('\n').length)); + }, [value]); + + const onTextFieldChange = async (v: string) => { + setValue(v); + }; + + const save = async () => { + await cellController?.saveCellData(value); + }; + + return ( +
+